Хорошая практика для аргументов ref и проверки, допускающей значение NULL - PullRequest
2 голосов
/ 17 июня 2020

Какой подход лучше всего использовать, когда я сталкиваюсь с ситуациями, подобными приведенным ниже? метод с ключевым словом ref. Настоящий код более сложен, но может быть более простой вариант:

private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass _initializationTarget;  //suggestion to mark as nullable

public MyClass GetInstance()
{
    return LazyInitializer.EnsureInitialized(
        ref _initializationTarget,
        ref _initialized,
        ref _initializationLock,
        () => new MyClass()
        );
}

Метод EnsureInitialized() принимает ссылку _initializationTarget, которая в начале равна null. , и, следовательно, отметка его как допускающего значение NULL кажется правильной корректировкой. Однако тот же метод обеспечивает правильное заполнение переменной.

Я не мог найти лучшего шаблона, чем следующий, но действительно ли он лучший?

private static bool _initialized = false;
private static object _initializationLock = new object();
private static MyClass? _initializationTarget;  //marked as nullable

public MyClass GetInstance()
{
    //the return value must also nullable
    MyClass? inst = LazyInitializer.EnsureInitialized(
        ref _initializationTarget,
        ref _initialized,
        ref _initializationLock,
        () => new MyClass()
        );

    return inst!;  //null-forgive here
}

1 Ответ

2 голосов
/ 23 июня 2020

TL; DR: Если вы удалите аргумент initialized, то EnsureInitialized() будет гарантировать, что возвращаемый объект [NotNull] - даже если вы передадите унифицированный MyClass? ссылка - и, следовательно, нет необходимости использовать оператор, допускающий нулевое значение (!).


Полный ответ

Это хороший вопрос. Ответ на ваш пример specifici c действительно прост, но я также хочу воспользоваться возможностью, чтобы ответить на ваш общий вопрос о том, как обрабатывать ref аргументы с помощью C# Ссылочные типы 8.0, допускающие значение NULL. При этом это не только поможет решить другой сценарий ios, подобный этому, но также предложит объяснение , почему работает решение для вашего конкретного c примера.

Specifi c Пример

Хотя документация EnsureInitialized() не проясняет это полностью, конкретная перегрузка, которую вы вызываете, предназначена для ситуаций, когда вы потенциально хотите a null цель. А именно, если вы вместо этого передадите значение true для параметра initialized, тогда он вернет null. Это условие является причиной того, что должен возвращать тип, допускающий значение NULL.

Поскольку вы не хотите разрешать null значения - и не работаете со значением type - вам просто нужно удалить аргумент initialized. Вам все равно нужно будет объявить свой _initializationTarget как допускающий значение NULL, поскольку он не инициализируется в вашем конструкторе. Эта перегрузка, однако, гарантирует компилятору, что параметр target больше не будет иметь значение null после выполнения EnsureInitialized():

private static object _initializationLock = new object();
private static MyClass? _initializationTarget;  //marked as nullable

public MyClass GetInstance() =>
    LazyInitializer.EnsureInitialized(
        ref _initializationTarget,
        ref _initializationLock,
        () => new MyClass()
    );

Обратите внимание, что пока он все еще передает значение null (может) MyClass?, он уверенно возвращает MyClass без необходимости прибегать к оператору , допускающему нулевые значения (!).

Общий вопрос

При погружении Далее в ссылочных типах C# 8.0, допускающих значение NULL, вы обнаружите ряд пробелов в анализе потока c Roslyn, которые нельзя устранить неоднозначностью, просто используя только операторы ? и !. К счастью, Microsoft предвидела эту проблему и предоставила нам множество атрибутов , которые можно использовать для предоставления подсказок компилятору .

Пример

Вот базовый пример c для иллюстрируют общую проблему:

public void EnsureNotNull(ref Object? input) => input ??= new Object();

Если вы вызовете этот метод с помощью следующего кода, вы получите предупреждение CS8602:

Object? object = null;
EnsureNotNull(ref object);
_ = object.ToString(); //CS8602; Dereference of a possibly null reference

Однако вы можете смягчить это, применив подсказку [NotNull] к параметру input:

public void EnsureNotNull([NotNull]ref Object? input) => input ??= new Object();

Теперь любые ссылки на object после вызова EnsureNotNull() будут известны (компилятором ), чтобы не быть null.

EnsureInitialized() перегрузок

Если вы оцениваете исходный код LazyInitializer с учетом вышеизложенного, ответ на ваш specifici c проблема становится намного яснее. Перегрузка , которую вы вызывали , отмечает target как [AllowNull], что эквивалентно возврату MyClass?:

public static T EnsureInitialized<T>([AllowNull] ref T target, ref bool initialized, [NotNull] ref object? syncLock, Func<T> valueFactory) => …

Напротив, перегрузка, которую я ' Рекомендуемый реализует атрибут [NotNull], описанный выше, что эквивалентно возврату MyClass:

public static T EnsureInitialized<T>([NotNull] ref T? target, [NotNull] ref object? syncLock, Func<T> valueFactory) where T : class => …

Если вы оцените фактическое значение logi c, вы увидите, что они в основном то же самое, за исключением того, что первое включает предложение escape для сценария, где initialized равно true - и, следовательно, позволяет target потенциально оставаться null.

Поскольку вы знаете , что вы работаете с классом, и не хотите значение null, однако последняя перегрузка является лучшим выбором и реализует точную практику, изложенную в моем ответе на ваш общий вопрос.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...