C # 8 ненульові посилання та шаблон "Спробуйте"


23

У класах C # є зразок, який ілюструється методом Dictionary.TryGetValueта int.TryParse: методом, який повертає булеве значення, що вказує на успіх операції, і вихідний параметр, що містить фактичний результат; якщо операція не вдалася, параметр «out» встановлюється на нуль.

Припустимо, я використовую ненульові посилання C # 8 і хочу написати метод TryParse для власного класу. Правильна підпис полягає в наступному:

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

Оскільки результат є недійсним у хибному випадку, змінна out повинна бути позначена як нульова.

Однак модель "Спроба" зазвичай використовується так:

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

Оскільки я входжую у гілку лише тоді, коли операція проходить успішно, результат ніколи не повинен бути нульовим у цій гілці. Але оскільки я позначив це як нульовий, тепер я повинен або перевірити це, або використовувати !для переосмислення:

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з типом значення, яке не зводиться до нуля?


Я не пробував цього (тому я не пишу це як відповідь), але, маючи нову функцію узгодження шаблону операторів перемикання, я здогадуюсь, один із варіантів - просто повернути нульову посилання (немає спробу-шаблону, return MyClass?), і зробіть на ньому перемикач з a 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обмежені класом або структурою як нульові типи значень - це зовсім інша річ з точки зору 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просторі, який ви можете використовувати для схеми спробу.

Наприклад:

як із цим справляється загальний код на зразок 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);

є

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

Однак насправді немає нічого, що перешкоджає присвоєнню нулю параметру out у старому стилі функцій TryParse.

напр.

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

Попередження, яке надається програмісту, коли вони використовують вихідний параметр без перевірки, є правильним. Ви повинні перевірити!

Буде багато випадків, коли ви будете змушені повертати нульові типи, коли головна гілка коду повертає ненульовий тип. Попередження якраз існує, щоб допомогти вам зробити це явним. тобто.

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

Ненульовий спосіб його кодування викине виняток там, де був би нуль. Незалежно від вашого розбору, отримання чи спочатку

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

Я не думаю, що FirstOrDefaultйого можна порівняти, оскільки нульове значення його повернення є основним сигналом. У TryParseметодах вихідний параметр, який не є нульовим, якщо значення повернення є істинним, є частиною контракту методу.
Себастьян Редл

це не є частиною договору. Єдине, що впевнене - це те, що щось присвоєно параметру out
Ewan

Таку поведінку я очікую від TryParseметоду. Якщо IPAddress.TryParseколи-небудь повернув true, але не призначив не-null своєму параметру out, я би повідомив про це як про помилку.
Себастьян Редл

Ваші очікування зрозумілі, але компілятор його не виконує. Так що, специфікація для IpAddress може сказати, що ніколи не повертає значення true та null, але мій приклад JsonObject показує випадок, коли повернення null може бути правильним
Ewan

"Ваше очікування зрозуміле, але компілятор його не виконує." - Я знаю, але моє запитання полягає в тому, як найкраще написати свій код для вираження договору, який я маю на увазі, а не те, що є контрактом якогось іншого коду.
Себастьян Редл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.