C # 8 ненулевые ссылки и образец Try

23

Существует закономерность в классах C # на примере Dictionary.TryGetValueи int.TryParse: метод , который возвращает логическое значение , указывающее успех операции и параметра из содержащего фактический результат; если операция не удалась, для параметра out устанавливается значение null.

Давайте предположим, что я использую ненулевые ссылки C # 8 и хочу написать метод TryParse для моего собственного класса. Правильная подпись заключается в следующем:

public static bool TryParse(string s, out MyClass? result);

Поскольку в ложном случае результат равен нулю, переменная out должна быть помечена как nullable.

Тем не менее, шаблон Try обычно используется так:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Поскольку я вхожу только в ветвь, когда операция завершается успешно, результат в этой ветке никогда не должен быть нулевым. Но поскольку я пометил его как nullable, теперь я должен либо проверить это, либо использовать !для переопределения:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

Это некрасиво и немного неэргономично.

Из-за типичного шаблона использования у меня есть еще один вариант: ложь о типе результата:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Теперь типичное использование становится лучше:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

но нетипичное использование не получает предупреждение, оно должно:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Теперь я не уверен, куда идти. Есть ли официальная рекомендация от языковой команды C #? Есть ли какой-либо код CoreFX, уже преобразованный в необнуляемые ссылки, который может показать мне, как это сделать? (Я искал TryParseметоды. IPAddressЭто класс, в котором он есть, но он не был преобразован в основную ветку corefx.)

И как Dictionary.TryGetValueс этим справляется общий код ? (Возможно, со специальным MaybeNullатрибутом из того, что я нашел.) Что произойдет, когда я создаю экземпляр Dictionaryс ненулевым типом значения?

Себастьян Редл
источник
Не пробовал (именно поэтому я не пишу это как ответ), но с новой функцией сопоставления с образцом операторов switch, я предполагаю, что один из вариантов - просто вернуть обнуляемую ссылку (без Try-pattern, вернуться MyClass?), и переключиться на него с помощью case MyClass myObj:и (опция все) case null:.
Филипп Милованович
+1 Мне очень нравится этот вопрос, и когда мне приходилось работать с этим, я всегда просто использовал дополнительную нулевую проверку вместо переопределения - которая всегда казалась немного ненужной и не элегантной, но это никогда не было в коде, критичном к производительности так что я просто отпустил это. Было бы приятно видеть, есть ли более чистый способ справиться с этим!
BrianH

Ответы:

10

Как вы описываете, шаблон bool / out-var не очень хорошо работает со ссылочными типами, допускающими обнуляемость. Поэтому вместо того, чтобы бороться с компилятором, используйте эту функцию, чтобы упростить вещи. Добавьте в C # 8 улучшенные возможности сопоставления с образцом, и вы можете рассматривать нулевую ссылку как «тип бедняка»:

public static MyClass? TryParse(string s) => 



if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

Таким образом, вы избегаете возиться с outпараметрами и не должны бороться с компилятором за смешивание nullс необнуляемыми ссылками.

И как Dictionary.TryGetValueс этим справляется общий код ?

В этот момент этот «тип бедного человека» падает. Проблема, с которой вы столкнетесь, заключается в том, что при использовании ссылочных типов, допускающих обнуляемость (NRT), компилятор будет обрабатываться Foo<T>как ненулевые. Но попробуйте изменить его на, Foo<T?>и он захочет, чтобы это Tограничение класса или структуры, поскольку типы значений, допускающие значение NULL, с точки зрения CLR - это совсем другое. Есть множество способов обойти это:

  1. Не включайте функцию NRT,
  2. Начните использовать default(вместе с !) для outпараметров, даже если ваш код не имеет нулевых значений,
  3. Используйте реальный Maybe<T>тип в качестве возвращаемого значения, которое тогда никогда не будет nullи оборачивает это boolи out Tв HasValueи Valueсвойства или некоторые такие,
  4. Используйте кортеж:
public static (bool success, T result) TryParse<T>(string s) => 


if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Лично я одобряю использование, Maybe<T>но поддерживаю деконструкцию, чтобы можно было сопоставить шаблон как кортеж, как в 4, выше.

Дэвид Арно
источник
2
TryParse(someString) is {} myClass- К этому синтаксису потребуется некоторое привыкание, но мне нравится идея.
Себастьян Редл
TryParse(someString) is var myClassвыглядит проще для меня.
Оливье Жако-Дескомб
2
@ OlivierJacot-Descombes это может выглядеть проще ... но это не сработает. Шаблон автомобиля всегда совпадает, поэтому x is var yвсегда будет верным, независимо от того, xявляется ли он нулевым или нет.
Дэвид Арно
15

Если вы приходите к этому немного поздно, как я, оказывается, что команда .NET обратилась к нему с помощью набора атрибутов параметров, как MaybeNullWhen(returnValue: true)в System.Diagnostics.CodeAnalysisпространстве, которое вы можете использовать для шаблона try.

Например:

Как общий код, например Dictionary.TryGetValue справляется с этим?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

а это значит, что вас кричат, если вы не проверяете true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Более подробная информация:

Ник Дарви
источник
3

Я не думаю, что здесь есть конфликт.

Ваше возражение против

public static bool TryParse(string s, out MyClass? result);

является

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

Однако на самом деле ничто не мешает присвоить значение null параметру out в функциях TryParse старого стиля.

например.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

Предупреждение, выдаваемое программисту, когда он использует параметр out без проверки, является правильным. Вы должны проверять!

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

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

Не допускающий обнуления способ его кодирования вызовет исключение, в котором он был бы нулевым. Будь то разбор, получение или первый

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
источник
Я не думаю, что FirstOrDefaultможно сравнивать, потому что нулевой его возвращаемое значение является основным сигналом. В TryParseметодах параметр out, не равный нулю, если возвращаемое значение равно true, является частью контракта метода.
Себастьян Редл
это не часть контракта. единственное, что гарантировано, это то, что что-то назначено параметру out
Ewan
Это поведение, которое я ожидаю от TryParseметода. Если IPAddress.TryParseкогда-либо возвращать значение true, но не назначать ненулевое значение для его параметра out, я сообщу об этом как об ошибке.
Себастьян Редл
Ваше ожидание понятно, но оно не обеспечивается компилятором. Конечно, в спецификации для IpAddress можно сказать, что никогда не возвращается true и null, но мой пример JsonObject показывает случай, когда возвращение null может быть правильным
Ewan
«Ваше ожидание понятно, но оно не обеспечивается компилятором». - Я знаю, но мой вопрос в том, как лучше написать мой код, чтобы выразить контракт, который я имею в виду, а не в том, каков контракт какого-то другого кода.
Себастьян Редл