C # tidak dapat membuat tipe `notnull` nullable

9

Saya mencoba membuat jenis yang mirip dengan Rust Resultatau Haskell Eitherdan saya sudah sejauh ini:

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

Mengingat bahwa kedua tipe parameter dibatasi notnull, mengapa mengeluh (di mana saja ada parameter tipe dengan ?tanda nullable setelahnya) bahwa:

Parameter tipe nullable harus diketahui sebagai tipe nilai atau tipe referensi yang tidak dapat dibatalkan. Pertimbangkan menambahkan 'kelas', 'struct', atau ketik batasan.

?


Saya menggunakan C # 8 di .NET Core 3 dengan jenis referensi nullable diaktifkan.

Sepatu Diamente
sumber
Anda harus mulai dari jenis hasil F # sebagai gantinya, dan serikat terdiskriminasi. Anda dapat dengan mudah mencapai sesuatu yang serupa di C # 8, tanpa membawa nilai mati, tetapi Anda tidak akan memiliki kecocokan lengkap. Mencoba menempatkan kedua jenis di struct yang sama akan mengalami satu masalah setelah yang lainnya, dan membawa kembali masalah yang seharusnya diperbaiki
Panagiotis Kanavos

Jawaban:

12

Pada dasarnya Anda meminta sesuatu yang tidak dapat direpresentasikan dalam IL. Tipe nilai nullable dan tipe referensi nullable adalah beast yang sangat berbeda, dan walaupun mereka terlihat serupa dalam kode sumber, IL sangat berbeda. Versi nullable dari tipe nilai Tadalah tipe yang berbeda ( Nullable<T>) sedangkan versi nullable dari tipe referensi Tadalah tipe yang sama , dengan atribut yang memberitahu kompiler apa yang diharapkan.

Pertimbangkan contoh sederhana ini:

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

Itu tidak valid karena alasan yang sama.

Jika kita membatasi Tmenjadi struct, maka IL yang dihasilkan untuk GetNullValuemetode akan memiliki tipe return Nullable<T>.

Jika kita membatasi Tuntuk menjadi tipe referensi yang tidak dapat dibatalkan, maka IL yang dihasilkan untuk GetNullValuemetode tersebut akan memiliki tipe pengembalian T, tetapi dengan atribut untuk aspek nullability.

Kompiler tidak dapat menghasilkan IL untuk metode yang memiliki tipe pengembalian keduanya Tdan Nullable<T>pada saat yang sama.

Ini pada dasarnya semua hasil dari jenis referensi nullable sama sekali bukan konsep CLR - itu hanya kompiler ajaib untuk membantu Anda mengekspresikan niat dalam kode dan membuat kompiler melakukan beberapa pemeriksaan pada waktu kompilasi.

Pesan kesalahannya tidak sejelas mungkin. Tdikenal sebagai "tipe nilai atau tipe referensi yang tidak dapat dibatalkan". Pesan kesalahan yang lebih tepat (tapi jauh lebih nyata) adalah:

Parameter tipe nullable harus diketahui sebagai tipe nilai, atau dikenal sebagai tipe referensi yang tidak dapat dibatalkan. Pertimbangkan menambahkan 'kelas', 'struct', atau ketik batasan.

Pada saat itu kesalahan akan berlaku pada kode kami - parameter tipe tidak "dikenal sebagai tipe nilai" dan itu tidak "diketahui sebagai tipe referensi yang tidak dapat dibatalkan". Ini dikenal sebagai salah satu dari dua, tetapi kompiler perlu tahu yang mana .

Jon Skeet
sumber
Ada juga runtime-magic - Anda tidak dapat membuat nullable nullable, meskipun tidak ada cara untuk menyatakan batasan itu di IL. Nullable<T>adalah tipe khusus yang tidak dapat Anda buat sendiri. Dan kemudian ada poin bonus tentang bagaimana tinju dilakukan dengan tipe nulllable.
Luaan
1
@Luaan: Ada sihir runtime untuk tipe nilai nullable, tetapi tidak untuk tipe referensi nullable.
Jon Skeet
6

Alasan untuk peringatan dijelaskan di bagian The issue with T?dari Try out Jenis Nullable Referensi . Singkat cerita, jika Anda menggunakan T?Anda harus menentukan apakah jenisnya adalah kelas atau struct. Anda dapat membuat dua jenis untuk setiap kasus.

Masalah yang lebih dalam adalah bahwa menggunakan satu jenis untuk mengimplementasikan Hasil dan menahan nilai Sukses dan Kesalahan mengembalikan masalah yang sama Hasil seharusnya diperbaiki, dan beberapa lagi.

  • Tipe yang sama harus membawa nilai mati, baik tipe atau kesalahan, atau mengembalikan nol
  • Pencocokan pola pada tipe tidak dimungkinkan. Anda harus menggunakan beberapa ekspresi pencocokan pola posisi mewah untuk membuatnya bekerja.
  • Untuk menghindari null, Anda harus menggunakan sesuatu seperti Option / Maybe, mirip dengan Opsi F # . Anda masih membawa Tidak ada di sekitar, baik untuk nilai atau kesalahan.

Hasil (dan Baik) di F #

Titik awal haruslah tipe Hasil F # dan serikat yang didiskriminasi. Bagaimanapun, ini sudah berfungsi di .NET.

Jenis hasil dalam F # adalah:

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

Jenis-jenis itu sendiri hanya membawa apa yang mereka butuhkan.

DU dalam F # memungkinkan pencocokan pola yang lengkap tanpa membutuhkan nol:

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

Meniru ini dalam C # 8

Sayangnya, C # 8 belum memiliki DU, mereka dijadwalkan untuk C # 9. Di C # 8 kita dapat meniru ini, tetapi kami kehilangan pencocokan lengkap:

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

Dan gunakan itu:

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

Tanpa pencocokan pola yang lengkap, kita harus menambahkan klausa default untuk menghindari peringatan kompiler.

Saya masih mencari cara untuk mendapatkan kecocokan lengkap tanpa memperkenalkan nilai mati, bahkan jika mereka hanya sebuah Opsi.

Opsi / Mungkin

Membuat kelas Opsi dengan cara yang menggunakan pencocokan lengkap lebih mudah:

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

Yang dapat digunakan dengan:

string cateGory = someValue switch { Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   };
Panagiotis Kanavos
sumber