C # 's không thể làm cho `notnull` gõ nullable


9

Tôi đang cố gắng tạo một loại tương tự như của Rust Resulthoặc Haskell Eithervà tôi đã đạt được điều này:

public struct Result<TResult, TError>
    where TResult : notnull
    where TError : notnull
{
    private readonly OneOf<TResult, TError> Value;
    public Result(TResult result) => Value = result;
    public Result(TError error) => Value = error;

    public static implicit operator Result<TResult, TError>(TResult result)
        => new Result<TResult, TError>(result);

    public static implicit operator Result<TResult, TError>(TError error)
        => new Result<TResult, TError>(error);

    public void Deconstruct(out TResult? result, out TError? error)
    {
        result = (Value.IsT0) ? Value.AsT0 : (TResult?)null;
        error = (Value.IsT1) ? Value.AsT1 : (TError?)null;
    }  
}

Cho rằng cả hai loại tham số đều bị hạn chế notnull, tại sao nó lại phàn nàn (bất cứ nơi nào có tham số loại có ?dấu nullable sau nó):

Tham số loại nullable phải được biết là loại giá trị hoặc loại tham chiếu không nullable. Xem xét thêm một 'lớp', 'struct' hoặc ràng buộc kiểu.

?


Tôi đang sử dụng C # 8 trên .NET Core 3 với các loại tham chiếu không thể kích hoạt.


Thay vào đó, bạn nên bắt đầu từ loại kết quả của F # và các hiệp hội phân biệt đối xử. Bạn có thể dễ dàng đạt được một cái gì đó tương tự trong C # 8, mà không mang theo giá trị chết, nhưng bạn sẽ không có kết hợp hoàn hảo. Cố gắng đặt cả hai loại trong cùng một cấu trúc sẽ gặp phải một vấn đề khác và mang lại những vấn đề rất khó Kết quả được cho là sẽ khắc phục
Panagiotis Kanavos

Câu trả lời:


12

Về cơ bản, bạn đang yêu cầu một cái gì đó không thể được đại diện trong IL. Các loại giá trị Nullable và các loại tham chiếu nullable là các con thú rất khác nhau và trong khi chúng trông giống nhau trong mã nguồn, IL lại rất khác nhau. Phiên bản nullable của một loại giá trị Tlà một loại khác ( Nullable<T>) trong khi phiên bản nullable của loại tham chiếu Tcùng loại, với các thuộc tính cho trình biên dịch biết những gì mong đợi.

Hãy xem xét ví dụ đơn giản hơn này:

public class Foo<T> where T : notnull
{
    public T? GetNullValue() => 
}

Điều đó không hợp lệ vì lý do tương tự.

Nếu chúng ta ràng buộc Tlà một cấu trúc, thì IL được tạo cho GetNullValuephương thức sẽ có kiểu trả về Nullable<T>.

Nếu chúng ta ràng buộc Tlà một kiểu tham chiếu không thể rỗng, thì IL được tạo cho GetNullValuephương thức sẽ có kiểu trả về T, nhưng với một thuộc tính cho khía cạnh nullable .

Trình biên dịch không thể tạo IL cho một phương thức có kiểu trả về của cả hai TNullable<T>cùng một lúc.

Về cơ bản, đây là tất cả kết quả của các loại tham chiếu không thể hoàn toàn không phải là một khái niệm CLR - đó chỉ là phép thuật của trình biên dịch để giúp bạn thể hiện ý định trong mã và khiến trình biên dịch thực hiện một số kiểm tra tại thời gian biên dịch.

Thông báo lỗi không rõ ràng như nó có thể được mặc dù. Tđược biết đến là "một loại giá trị hoặc loại tham chiếu không thể rỗng". Một thông báo lỗi chính xác hơn (nhưng đáng kể hơn) sẽ là:

Tham số loại nullable phải được biết là loại giá trị hoặc được biết là loại tham chiếu không thể rỗng. Xem xét thêm một 'lớp', 'struct' hoặc ràng buộc kiểu.

Tại thời điểm đó, lỗi sẽ áp dụng một cách hợp lý cho mã của chúng tôi - tham số loại không "được biết là loại giá trị" và nó không "được biết đến là loại tham chiếu không thể rỗng". Nó được biết là một trong hai, nhưng trình biên dịch cần biết cái nào .


Cũng có ma thuật thời gian chạy - bạn không thể tạo thành một nullable nullable, mặc dù không có cách nào để thể hiện sự hạn chế đó trong IL. Nullable<T>là một loại đặc biệt mà bạn không thể tự làm. Và sau đó là điểm thưởng về cách đấm bốc được thực hiện với các loại không thể.
Luaan

1
@Luaan: Có ma thuật thời gian chạy cho các loại giá trị nullable, nhưng không dành cho các loại tham chiếu nullable.
Jon Skeet

6

Lý do cho sự cảnh báo được giải thích trong phần The issue with T?của thử loại Nullable tham khảo . Câu chuyện dài, nếu bạn sử dụng, T?bạn phải xác định loại là lớp hay cấu trúc. Bạn có thể sẽ tạo ra hai loại cho mỗi trường hợp.

Vấn đề sâu xa hơn là việc sử dụng một loại để triển khai Kết quả và giữ cả hai giá trị Thành công và Lỗi mang lại cùng một vấn đề Kết quả được cho là phải khắc phục và một vài vấn đề khác.

  • Loại tương tự phải mang một giá trị chết xung quanh, loại hoặc lỗi hoặc mang lại null
  • Khớp mẫu trên loại không thể. Bạn sẽ phải sử dụng một số biểu thức khớp mẫu vị trí ưa thích để làm việc này.
  • Để tránh null, bạn sẽ phải sử dụng một cái gì đó như Tùy chọn / Có thể, tương tự như Tùy chọn của F # . Mặc dù vậy, bạn vẫn sẽ mang theo Không có giá trị hoặc lỗi.

Kết quả (và Either) trong F #

Điểm bắt đầu phải là loại Kết quả của F # và các hiệp hội phân biệt đối xử. Rốt cuộc, điều này đã hoạt động trên .NET.

Loại kết quả trong F # là:

type Result<'T,'TError> =
    | Ok of ResultValue:'T
    | Error of ErrorValue:'TError

Các loại bản thân chỉ mang những gì họ cần.

Các DU trong F # cho phép khớp mẫu đầy đủ mà không yêu cầu null:

match res2 with
| Ok req -> printfn "My request was valid! Name: %s Email %s" req.Name req.Email
| Error e -> printfn "Error: %s" e

Thi đua này trong C # 8

Thật không may, C # 8 chưa có DU, chúng được lên lịch cho C # 9. Trong C # 8, chúng tôi có thể mô phỏng điều này, nhưng chúng tôi mất kết hợp hoàn hảo:

#nullable enable

public interface IResult<TResult,TError>{}​

struct Success<TResult,TError> : IResult<TResult,TError>
{
    public TResult Value {get;}

    public Success(TResult value)=>Value=value;

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

struct Error<TResult,TError> : IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error)=>ErrorValue=error;

    public void Deconstruct(out TError error)=>error=ErrorValue;
}

Và sử dụng nó:

IResult<double,string> Sqrt(IResult<double,string> input)
{
    return input switch {
        Error<double,string> e => e,
        Success<double,string> (var v) when v<0 => new Error<double,string>("Negative"),
        Success<double,string> (var v)  => new Success<double,string>(Math.Sqrt(v)),
        _ => throw new ArgumentException()
    };
}

Nếu không khớp mẫu đầy đủ, chúng ta phải thêm mệnh đề mặc định đó để tránh các cảnh báo của trình biên dịch.

Tôi vẫn đang tìm cách để có được kết hợp hoàn hảo mà không đưa ra các giá trị chết, ngay cả khi chúng chỉ là một Tùy chọn.

Tùy chọn / Có thể

Tạo một lớp Tùy chọn bằng cách sử dụng kết hợp toàn diện đơn giản hơn:

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

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

Mà có thể được sử dụng với:

string cateGory = someValue switch { Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   };
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.