C#8非空引用和Try模式


22

在C#类中有一个模式,以Dictionary.TryGetValue和表示int.TryParse:一种方法,该方法返回指示操作成功的布尔值和包含实际结果的out参数;如果操作失败,则将out参数设置为null。

假设我正在使用C#8不可为空的引用,并且想为自己的类编写一个TryParse方法。在正确的签名是这样的:

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

因为在错误情况下结果为null,所以必须将out变量标记为可为空。

但是,通常使用Try模式,如下所示:

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

因为我仅在操作成功时才​​进入分支,所以结果在该分支中永远不能为null。但是因为我将其标记为可为空,所以现在我必须检查它或使用它!来覆盖:

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的master分支上进行转换。)

像这样的通用代码如何Dictionary.TryGetValue处理呢?(可能具有MaybeNull我发现的特殊属性。)当我实例化Dictionary具有非空值类型的a 时会发生什么?


没尝试过(这就是为什么我不写这个作为答案),但是有了switch语句的新模式匹配功能,我猜一个选择就是简单地返回可为空的引用(没有Try-pattern, return MyClass?),并使用case MyClass myObj:和(可选)在其上进行切换case null:
FilipMilovanović

+1我真的很喜欢这个问题,当我不得不使用它时,我总是只使用了一个额外的空值检查而不是覆盖(override)-这总是感觉有些不必要和不雅观,但是它从来都不是对性能有严格要求的代码所以我就放手 希望能找到一种更清洁的处理方式!
BrianH

Answers:


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和包装了boolout THasValueValue性质或一些这样的,
  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在我看来更容易。
Olivier Jacot-Descombes

2
@ OlivierJacot-Descombes可能看起来更容易...但是它不起作用。汽车模式始终匹配,因此x is var y无论是否x为null 都将始终为true 。
David Arno

15

如果您像我这样迟到,那么事实证明.NET团队会通过一系列参数属性(例如您可以用于try模式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);

因为我仅在操作成功时才​​进入分支,所以结果在该分支中永远不能为null。

但是,实际上,没有什么可以阻止在旧式TryParse函数中将null分配给out参数。

例如。

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

当程序员使用out参数而不进行检查时,给程序员的警告是正确的。您应该检查一下!

在很多情况下,您将被迫返回可为空的类型,而代码的主分支将返回不可为空的类型。警告只是为了帮助您使这些内容明确。即。

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

使用非null进行编码的方式会在存在null的地方引发异常。无论您是解析,获取还是初学者

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

我不认为FirstOrDefault可以比较,因为其返回值的无效主要信号。在TryParse方法中,如果返回值为true,则out参数不为null是方法协定的一部分。
塞巴斯蒂安·雷德尔

它不是合同的一部分。唯一可以确定的是,已为out参数分配了某些内容
Ewan

这是我期望的一种TryParse方法的行为。如果IPAddress.TryParse曾经返回true,但没有为其out参数分配非null,则我将其报告为错误。
塞巴斯蒂安·雷德尔

您的期望是可以理解的,但编译器不会强制执行。可以肯定的是,IpAddress的规范可能会说从不返回true和null,但是我的JsonObject示例显示了返回null可能是正确的情况
Ewan

“您的期望是可以理解的,但编译器不会强制执行。” -我知道,但是我的问题是如何最好地编写我的代码来表达我所想到的契约,而不是其他代码的契约。
塞巴斯蒂安·雷德尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.