想要启用可空引用类型时如何处理可选参数?

How to deal with optional arguments when wanting to enable nullable reference types?

我看到了启用(非)可空引用类型的巨大优势,但我有很多带有可选参数的方法,我想知道更正编译器产生的警告的正确方法是什么。

通过使用 ? 注释类型使参数可为 null 会带走所有优点。另一个想法是将所有带有可选参数的方法变成单独的方法,这是相当多的工作并且会产生高复杂性(参数组合的指数爆炸)。

我正在考虑这样的事情,但我真的很怀疑这是否是一种超越第一眼的好方法(性能方面等):

[Fact]
public void Test()
{
  Assert.Equal("nothing", Helper().ValueOrFallbackTo("nothing"));
  Assert.Equal("foo", Helper("foo").ValueOrFallbackTo("whatever"));
}

public static Optional<string> Helper(Optional<string> x = default)
{
  return x;
}

public readonly ref struct Optional<T>
{
  private readonly bool initialized;
  private readonly T value;

  public Optional(T value)
  {
    initialized = true;
    this.value = value;
  }

  public T ValueOrFallbackTo(T fallbackValue)
  {
    return initialized ? value : fallbackValue;
  }

  public static implicit operator Optional<T>(T value)
  {
    return new Optional<T>(value);
  }
}

这看起来像 F# 的选项。这可以在 C# 8 中使用模式匹配表达式进行模拟。这个结构:

readonly struct Option<T> 
{
    public readonly T Value {get;}

    public readonly bool IsSome {get;}
    public readonly bool IsNone =>!IsSome;

    public Option(T value)=>(Value,IsSome)=(value,true);    

    public void Deconstruct(out T value)=>(value)=(Value);
}

//Convenience methods, similar to F#'s Option module
static class Option
{
    public static Option<T> Some<T>(T value)=>new Option<T>(value);    
    public static Option<T> None<T>()=>default;
    ...
}

应该允许这样的代码:

static string Test(Option<MyClass> opt = default)
{
    return opt switch
    {
            Option<MyClass> { IsNone: true } => "None",                
            Option<MyClass> (var v)          => $"Some {v.SomeText}",
    };
}

第一个选项使用 属性 模式匹配来检查 None,而第二个选项使用位置模式匹配通过解构器实际提取值。

好消息是编译器将此识别为穷举匹配,因此我们不需要添加默认子句。

不幸的是,Roslyn 错误 prevents this. The linked issue actually tries to create an Option class based on an abstract base class. This was fixed in VS 2019 16.4 Preview 1

固定编译器允许我们省略参数或传递一个None :

class MyClass
{
    public string SomeText { get; set; } = "";
}

...

Console.WriteLine( Test() );
Console.WriteLine( Test(Option.None<MyClass>()) );

var c = new MyClass { SomeText = "Cheese" };
Console.WriteLine( Test(Option.Some(c)) );

这会产生:

None
None
Some Cheese

VS 2019 16.4 应该会在几周内与 .NET Core 3.1 同时发布。

在那之前,一个更丑陋的解决方案可能是在解构函数中 return IsSome 并在两种情况下都使用位置模式匹配:

public readonly struct Option<T> 
{
    public readonly T Value {get;}

    public readonly bool IsSome {get;}
    public readonly bool IsNone =>!IsSome;

    public Option(T value)=>(Value,IsSome)=(value,true);    

    public void Deconstruct(out T value,out bool isSome)=>(value,isSome)=(Value,IsSome);
    public void Deconstruct(out T value)=>(value)=(Value);
}

    return opt switch {  Option<MyClass> (_    ,false)  =>"None",
                         Option<MyClass> (var v,true)   => $"Some {v.SomeText}" ,                };

借鉴 F# 选项

无论我们使用哪种技术,我们都可以向模仿 F# 的 Option moduleOption 静态 class 添加扩展方法,例如 Bind,也许是最有用的方法,应用如果它有一个值并且 returns 一个 Option,或者 returns None 如果没有值,则对 Option 起作用:

public static Option<U> Bind<T,U>(this Option<T> inp,Func<T,Option<U>> func)
{
    return inp switch {  Option<T> (_    ,false)  =>Option.None<U>(),
                         Option<T> (var v,true)   => func(v) ,                         
                       };
}

例如,这将 Format 方法应用于选项以创建 Optino :

Option<string> Format(MyClass c)
{
    return Option.Some($"Some {c.SomeText}");
}

var c=new MyClass { SomeText = "Cheese"};
var opt=Option.Some(c);
var message=opt.Bind(Format);

这使得创建其他辅助函数或产生选项的链函数变得容易