簡體   English   中英

C#.Net 中的可選返回

[英]Optional return in C#.Net

Java 1.8 正在接收Optional<T>類,它允許我們明確說明方法何時可以返回空值並“強制”其使用者在使用它之前驗證它是否不為空( isPresent() )。

我看到 C# 有Nullable<T> ,它做了類似的事情,但具有基本類型。 它似乎用於數據庫查詢,以區分值何時存在且為 0,何時不存在且為空。

但似乎 C# 的Nullable<T>不適用於對象,僅適用於基本類型,而 Java 的Optional<T>僅適用於對象,不適用於基本類型。

C# 中是否有 Nullable/Optional 類,它迫使我們在提取和使用對象之前測試對象是否存在?

不是在語言中,不,但您可以自己制作:

public struct Optional<T>
{
    public bool HasValue { get; private set; }
    private T value;
    public T Value
    {
        get
        {
            if (HasValue)
                return value;
            else
                throw new InvalidOperationException();
        }
    }

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

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

    public override bool Equals(object obj)
    {
        if (obj is Optional<T>)
            return this.Equals((Optional<T>)obj);
        else
            return false;
    }
    public bool Equals(Optional<T> other)
    {
        if (HasValue && other.HasValue)
            return object.Equals(value, other.value);
        else
            return HasValue == other.HasValue;
    }
}

請注意,您將無法模擬Nullable<T>某些行為,例如將沒有值的可空值裝箱為空的能力,而不是裝箱的可空值,因為它對此具有特殊的編譯器支持(以及其他一些)行為。

在我看來,任何公開HasValue屬性的Option實現都是對整個想法的失敗。 可選對象的要點是您可以無條件調用它們的內容,而無需測試內容是否存在。

如果您必須測試可選對象是否包含值,那么與常見的null測試相比,您沒有做任何新的事情。

這是我詳細解釋可選對象的文章: C# 中選項/可能類型的自定義實現

這是包含代碼和示例的 GitHub 存儲庫: https : //github.com/zoran-horvat/option

如果您不願意使用重量級的 Option 解決方案,那么您可以輕松構建一個輕量級的解決方案。 您可以創建自己的Option<T>類型來實現IEnumerable<T>接口,以便您可以利用 LINQ 擴展方法將調用變為可選。 這是最簡單的可能實現:

public class Option<T> : IEnumerable<T>
{
    private readonly T[] data;

    private Option(T[] data)
    {
        this.data = data;
    }

    public static Option<T> Create(T value)
    {
        return new Option<T>(new T[] { value });
    }

    public static Option<T> CreateEmpty()
    {
        return new Option<T>(new T[0]);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)this.data).GetEnumerator();
    }

    System.Collections.IEnumerator
        System.Collections.IEnumerable.GetEnumerator()
    {
        return this.data.GetEnumerator();
    }
}

使用這個Option<T>類型是通過 LINQ 完成的:

Option<Car> optional = Option<Car>.Create(myCar);
string color = optional
  .Select(car => car.Color.Name)
  .DefaultIfEmpty("<no car>")
  .Single();  // you can call First(), too

您可以在以下文章中找到有關可選對象的更多信息:

您可以參考我的視頻課程,了解有關如何使用Option類型和其他方式簡化控制流的更多詳細信息: 使您的 C# 代碼在 .NET 中更具功能性戰術性設計模式:控制流

第一個視頻課程( 使您的 C# 代碼更具功能性)詳細介紹了面向鐵路的編程,包括EitherOption類型以及如何使用它們來管理可選對象以及處理異常情況和錯誤。

C# 中有更好的選項類型實現。 您可以Zoran Horvat 的.NET中的Tactical design patterns in .NET 中找到此實現,網址為 Multiplesight.com。 它包括解釋為什么以及如何使用它。 基本思想是將選項類實現為 IEnumerable<> 接口的實現。

public class Option<T> : IEnumerable<T>
{
    private readonly T[] data;

    private Option(T[] data)
    {
        this.data = data;
    }

    public static Option<T> Create(T element)
    {
        return new Option<T>(new[] { element });
    }

    public static Option<T> CreateEmpty()
    {
        return new Option<T>(new T[0]);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>) this.data).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

沒有內置的東西,但你可以定義你自己的。 請注意,如果不定義映射/綁定運算符,則Option<T>實現沒有意義。

public struct Option<T>
{
    private bool hasValue;
    private T value;

    public Option(T value)
    {
        if (value == null) throw new ArgumentNullException("value");
        this.hasValue = true;
        this.value = value;
    }

    public Option<TOut> Select<TOut>(Func<T, TOut> selector)
    {
        return this.hasValue ? new Option<TOut>(selector(this.value)) : new Option<TOut>();
    }

    public Option<TOut> SelectMany<TOut>(Func<T, Option<TOut>> bind)
    {
        return this.hasValue ? bind(this.value) : new Option<TOut>();
    }

    public bool HasValue
    {
        get { return this.hasValue; }
    }

    public T GetOr(T @default)
    {
        return this.hasValue ? this.value : @default;
    }
}

在“C# 函數式語言擴展”項目https://github.com/louthy/language-ext 中存在 F# 的 Option 對象以及其他函數式模式

您可以使用FSharpCore.dll程序FSharpCore.dll Microsoft.FSharp.Core.FSharpOption<T> ,而不是編寫自己的類。 不幸的是,F# 類型在 C# 中使用時有點笨拙。

//Create
var none = FSharpOption<string>.None;
var some1 = FSharpOption<string>.Some("some1");
var some2 = new FSharpOption<string>("some2");

//Does it have value?
var isNone1 = FSharpOption<string>.get_IsNone(none);
var isNone2 = OptionModule.IsNone(none);
var isNone3 = FSharpOption<string>.GetTag(none) == FSharpOption<string>.Tags.None;

var isSome1 = FSharpOption<string>.get_IsSome(some1);
var isSome2 = OptionModule.IsSome(some1);
var isSome3 = FSharpOption<string>.GetTag(some2) == FSharpOption<string>.Tags.Some;

//Access value
var value1 = some1.Value; //NullReferenceException when None
var value2 = OptionModule.GetValue(some1); //ArgumentException when None

也許這更接近於 F# Option 類型

public struct Option<T>
{
    private T value;
    private readonly bool hasValue;

    public bool IsSome => hasValue;

    public bool IsNone => !hasValue;

    public T Value
    {
        get
        {
            if (!hasValue) throw new NullReferenceException();
            return value;
        }
    }

    public static Option<T> None => new Option<T>();

    public static Option<T> Some(T value) => new Option<T>(value);

    private Option(T value)
    {
        this.value = value;
        hasValue = true;
    }

    public TResult Match<TResult>(Func<T, TResult> someFunc, Func<TResult> noneFunc) =>
        hasValue ? someFunc(value) : noneFunc();

    public override bool Equals(object obj)
    {
        if (obj is Option<T>)
        {
            var opt = (Option<T>)obj;
            return hasValue ? opt.IsSome && opt.Value.Equals(value) : opt.IsNone;
        }
        return false;
    }

    public override int GetHashCode() =>
        hasValue ? value.GetHashCode() : 0;
}

前段時間我決定使用最新的 C# 版本之一來實現某種 Optional<> Java 類原型。

這里是:

public sealed class Optional<T>
{
    private static readonly Optional<T> EMPTY = new Optional<T>();
    private readonly T value;

    private Optional() => value = default;
    private Optional(T arg) => value = arg.RequireNonNull("Value should be presented");

    public static Optional<T> Empty() => EMPTY;
    public static Optional<T> Of(T arg) => new Optional<T>(arg);
    public static Optional<T> OfNullable(T arg) => arg != null ? Of(arg) : Empty();
    public static Optional<T> OfNullable(Func<T> outputArg) => outputArg != null ? Of(outputArg()) : Empty();

    public bool HasValue => value != null;

    public void ForValuePresented(Action<T> action) => action.RequireNonNull()(value);

    public IOption<T> Where(Predicate<T> predicate) => HasValue 
        ? predicate.RequireNonNull()(value) ? this : Empty() : this;

    public IOption<TOut> Select<TOut>(Func<T, TOut> select) => HasValue 
        ? Optional<TOut>.OfNullable(select.RequireNonNull()(value)) 
        : Optional<TOut>.Empty();

    public IOption<IOption<TOut>> SelectMany<TOut>(Func<T, IOption<TOut>> select) => HasValue 
        ? Optional<IOption<TOut>>.OfNullable(select.RequireNonNull()(value)) 
        : Optional<IOption<TOut>>.Empty();

    public T Get() => value;
    public T GetCustomized(Func<T, T> getCustomized) => getCustomized.RequireNonNull()(value);
    public U GetCustomized<U>(Func<T, U> getCustomized) => getCustomized.RequireNonNull()(value);

    public T OrElse(T other) => HasValue ? value : other;
    public T OrElseGet(Func<T> getOther) => HasValue ? value : getOther();
    public T OrElseThrow<E>(Func<E> exceptionSupplier) where E : Exception => HasValue ? value : throw exceptionSupplier();

    public static explicit operator T(Optional<T> optional) => OfNullable((T) optional).Get();
    public static implicit operator Optional<T>(T optional) => OfNullable(optional);

    public override bool Equals(object obj)
    {
        if (obj is Optional<T>) return true;
        if (!(obj is Optional<T>)) return false;
        return Equals(value, (obj as Optional<T>).value);
    }

    public override int GetHashCode() => base.GetHashCode();
    public override string ToString() => HasValue ? $"Optional has <{value}>" : $"Optional has no any value: <{value}>";
}

C# 中是否有 Nullable/Optional 類,它迫使我們在提取和使用對象之前測試對象是否存在?

創建 Nullables 以便原始類型可以為 null。 它們的默認值不必是實際值(像 int 一樣,如果沒有可空值,它的默認值為 0,那么 0 是指 0 還是未設置為 0?)

不,您無法強制程序員檢查對象是否為空。 不過這很好。 這樣做會產生大量的開銷。 如果這是一項語言功能,您多久會強制檢查一次? 首次分配變量時是否需要它? 如果變量稍后指向另一個對象怎么辦? 你會強制它在每個方法和屬性之前檢查,如果失敗你會拋出異常嗎? 你現在得到一個空引用異常。 強迫某人做超出你已有的事情,你不會得到什么好處。

從 Zoran Horvat 的回答中學到了很多。 這是我的代碼。 可選可以有一個真實的值或一個空的。 在消費方面,相同的代碼處理它們。

void Main()
{
    var myCar = new Car{ Color =  Color.Black, Make="Toyota"};

    Option<Car> optional = Option<Car>.Create(myCar);

    // optional is an Empty 50% of the time.
    if(new Random().NextDouble() > 0.5)
        optional = Option<Car>.CreateEmpty();



    string color = optional
    .Select(car => car.Color.Name)
    .DefaultIfEmpty("<no car>")
    .Single();
    Console.Write(color);
}

class Car {
    public Color Color { get; set; }
    public string Make { get; set;}
}

public class Option<T> : IEnumerable<T>
{
    private readonly T[] data;

    private Option(T[] data)
    {
        this.data = data;
    }

    public static Option<T> Create(T value)
    {
        return new Option<T>(new T[] { value });
    }

    public static Option<T> CreateEmpty()
    {
        return new Option<T>(new T[0]);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)this.data).GetEnumerator();
    }

    System.Collections.IEnumerator
        System.Collections.IEnumerable.GetEnumerator()
    {
        return this.data.GetEnumerator();
    }
}

使用T? 可為空引用而不是Option<T>

從 C#8 開始,您應該棄用自定義Option<T>實現。 零困境現已完全解決。

T? 是對Option<T>的完全替代

C# 具有以下處理 null 的特性:

  1. 空合並運算符
  2. 空條件運算符
  3. 不可為空和可為空的引用類型 (C#8 起)
  4. 可配置的編譯錯誤/警告

請記住

Option<Car> optional = Option<Car>.Create(myCar);
string color = optional
  .Select(car => car.Color.Name)
  .DefaultIfEmpty("<no car>")
  .Single();  // you can call First(), too

是相同的

string color = myCar?.Color.Name ?? "<no car>";

此外,字符串顏色也是一個不能為空的引用。

如果您不喜歡烘焙自己的解決方案,我會使用Language Ext 它在 nuget 上可用。 我最近開始使用這個庫,空引用的安全性是驚人的! 我不是這個庫的專家,但它可以滿足您的要求,等等。

以下是可以做的事情:

using System;
using LanguageExt;
using static LanguageExt.Prelude;

public class Demo
{
    public static Option<int> ToEvenOrNone(int i) =>
        i % 2 == 0
            ? i.Apply(Optional)
            : None;

    public static void PrintToDebug(Option<int> value) => 
        value
            .Some(Console.WriteLine)
            .None(() => Console.WriteLine("Value is not Even!"));

    public static void Test()
    {
        for (int i = 0; i < 10; i++)
        {
            PrintToDebug(ToEvenOrNone(i));
        }
    }
}

這是輸出:

0
Value is not Even!
2
Value is not Even!
4
Value is not Even!
6
Value is not Even!
8
Value is not Even!

https://github.com/mcintyre321/OneOf

我認為這個 oneOf 類很好地重新創建了選項類型。 它甚至包括一個 .switch/.match 與模式匹配,最重要的是它在運行時工作,這是你對 Option 模式的期望。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM