How to deal with optional arguments when wanting t

2020-02-15 10:40发布

I see the very great advantage of turning on (non-)nullable reference types, but I have quite a lot of methods with optional parameters and I am wondering what the right way is to correct the warnings yielded by the compiler in a wise way.

Making the parameter nullable by annotating the type with "?" takes all the goodness away. Another idea is to turn all methods with optional parameters into separate methods which is quite a lot of work or yields high complexity (exponential explosion of parameter combinations).

I was thinking about something like this, but I really question if that it a good approach (performance-wise etc.) beyond the first glance:

[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);
  }
}

1条回答
小情绪 Triste *
2楼-- · 2020-02-15 11:15

This look's like F#'s Option. This can be emulated in C# 8 up to a point with pattern matching expressions. This struct :

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;
    ...
}

Should allow code like this :

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

The first option uses property pattern matching to check for None, while the second one uses positional pattern matching to actually extract the value through the deconstructor.

The nice thing is that the compiler recognizes this as an exhaustive match so we don't need to add a default clause.

Unfortunately, a Roslyn bug 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.

The fixed compiler allows us to omit the parameter or pass a 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)) );

This produces :

None
None
Some Cheese

VS 2019 16.4 should come out at the same time as .NET Core 3.1 in a few weeks.

Until then, an uglier solution could be to return IsSome in the deconstructor and use positional pattern matching in both cases:

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);
}

And

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

Borrowing from F# Options

No matter which technique we use, we can add extension methods to the Option static class that mimic F#'s Option module, eg Bind, perhaps the most useful method, applies a function to an Option if it has a value and returns an Option, or returns None if there's no value :

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) ,                         
                       };
}

For example this applies the Format method to an Option to create a 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);

This makes it easy to create other helper functions, or chain functions that produce options

查看更多
登录 后发表回答