C # ไม่สามารถทำให้ `notnull 'ประเภทเป็นโมฆะ


9

ฉันพยายามสร้างประเภทที่คล้ายกับ Rust's Resultหรือ Haskell Eitherและฉันก็เข้าใจได้:

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

ระบุว่าพารามิเตอร์ทั้งสองประเภทถูก จำกัด ให้เป็นnotnullเพราะเหตุใดจึงบ่น (ที่ใดก็ตามที่มีพารามิเตอร์ประเภทที่มี?เครื่องหมายnullable หลังจากนั้น):

พารามิเตอร์ประเภท nullable ต้องทราบว่าเป็นประเภทค่าหรือประเภทการอ้างอิงที่ไม่ใช่ค่า null ลองเพิ่มข้อ จำกัด 'class', 'struct' หรือพิมพ์

?


ฉันใช้ C # 8 บน. NET Core 3 ที่เปิดใช้งานประเภทอ้างอิงที่เป็นโมฆะ


คุณควรเริ่มจากประเภทผลลัพธ์ของ F #แทนและเลือกปฏิบัติสหภาพ คุณสามารถบรรลุสิ่งที่คล้ายกันใน C # 8 ได้โดยง่ายโดยไม่ต้องคำนึงถึงคุณค่าที่ตายแล้ว แต่คุณจะไม่มีการจับคู่ที่ครบถ้วนสมบูรณ์ การพยายามใส่ทั้งสองประเภทใน struct เดียวกันจะพบปัญหาหนึ่งหลังจากที่อื่นและนำกลับมาปัญหามากผลควรแก้ไข
Panagiotis Kanavos

คำตอบ:


12

โดยทั่วไปคุณกำลังขอสิ่งที่ไม่สามารถแสดงใน IL ประเภทของค่า Nullable และประเภทอ้างอิง nullable เป็นสัตว์ที่แตกต่างกันมากและในขณะที่พวกเขามีลักษณะคล้ายกันในรหัสที่มา IL แตกต่างกันมาก รุ่น nullable ของประเภทค่าTเป็นประเภทที่แตกต่างกัน ( Nullable<T>) ในขณะที่รุ่น nullable ของประเภทอ้างอิงTเป็นประเภทเดียวกันที่มีคุณลักษณะบอกคอมไพเลอร์สิ่งที่คาดหวัง

ลองพิจารณาตัวอย่างที่ง่ายกว่านี้:

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

ไม่ถูกต้องด้วยเหตุผลเดียวกัน

ถ้าเราข้อ จำกัดTที่จะ struct แล้ว IL ที่สร้างขึ้นสำหรับวิธีการที่จะมีประเภทของการกลับมาของGetNullValueNullable<T>

หากเรา จำกัดTให้เป็นประเภทอ้างอิงที่ไม่เป็นโมฆะ IL ที่สร้างขึ้นสำหรับGetNullValueวิธีนี้จะมีประเภทส่งคืนTแต่มีแอททริบิวสำหรับมุมมองที่เป็นโมฆะ

คอมไพเลอร์ไม่สามารถสร้าง IL สำหรับวิธีที่มีชนิดส่งคืนของทั้งสองTและNullable<T>ในเวลาเดียวกัน

นี่เป็นผลลัพธ์ของประเภทการอ้างอิงแบบ nullable ทั้งหมดที่ไม่ได้เป็นแนวคิด CLR เลย - เป็นเพียงแค่เวทมนตร์คอมไพเลอร์เพื่อช่วยให้คุณแสดงเจตนาในโค้ดและรับคอมไพเลอร์เพื่อทำการตรวจสอบบางอย่างในเวลาคอมไพล์

ข้อความแสดงข้อผิดพลาดไม่ชัดเจนเท่าที่ควร Tเป็นที่รู้จักกันว่า "ประเภทค่าหรือประเภทอ้างอิงที่ไม่เป็นโมฆะ" ข้อความแสดงข้อผิดพลาดที่แม่นยำยิ่งขึ้น (แต่สำคัญกว่า wordier):

พารามิเตอร์ประเภท nullable ต้องทราบว่าเป็นประเภทค่าหรือทราบว่าเป็นประเภทอ้างอิงที่ไม่สามารถลบล้างได้ ลองเพิ่มข้อ จำกัด 'class', 'struct' หรือพิมพ์

ณ จุดนั้นข้อผิดพลาดจะนำไปใช้กับรหัสของเราอย่างสมเหตุสมผล - พารามิเตอร์ชนิดไม่เป็นที่รู้จักว่าเป็นประเภทค่าและไม่เป็นที่รู้จักกันว่าเป็นประเภทอ้างอิงที่ไม่เป็นโมฆะ เป็นที่รู้จักกันว่าเป็นหนึ่งในสอง แต่คอมไพเลอร์จำเป็นต้องรู้ซึ่ง


มีเวทย์มนตร์ด้วยเช่นกัน - คุณไม่สามารถทำให้เป็นโมฆะที่เป็นโมฆะแม้ว่าจะไม่มีวิธีใดที่จะแสดงถึงข้อ จำกัด ใน IL Nullable<T>เป็นประเภทพิเศษที่คุณทำเองไม่ได้ จากนั้นก็มีจุดโบนัสว่ามวยทำอะไรกับประเภท nulllable
Luaan

1
@Luaan: มีเวทย์มนตร์รันไทม์สำหรับประเภทค่า nullable แต่ไม่ใช่สำหรับประเภทการอ้างอิง nullable
Jon Skeet

6

เหตุผลในการแจ้งเตือนจะมีการอธิบายในส่วนThe issue with T?ของ ลองประเภท Nullable อ้างอิง เรื่องสั้นสั้นถ้าคุณใช้T?คุณต้องระบุว่าประเภทเป็นชั้นหรือ struct คุณสามารถสร้างสองประเภทสำหรับแต่ละกรณี

ปัญหาที่ลึกกว่าคือการใช้ประเภทหนึ่งเพื่อใช้ผลลัพธ์และถือทั้งค่าความสำเร็จและข้อผิดพลาดจะทำให้เกิดปัญหาเดียวกันผลลัพธ์ควรแก้ไขและอีกสองสามอย่าง

  • ประเภทเดียวกันจะต้องมีค่าตายตัวไม่ว่าจะเป็นชนิดหรือข้อผิดพลาดหรือนำกลับเป็นค่าว่าง
  • การจับคู่รูปแบบกับประเภทเป็นไปไม่ได้ คุณต้องใช้การจับคู่รูปแบบตำแหน่งแฟนซีเพื่อให้สิ่งนี้ทำงานได้
  • เพื่อหลีกเลี่ยงการ nulls คุณจะต้องใช้สิ่งที่ต้องการตัวเลือก / อาจจะคล้ายกับ F # 's ตัวเลือก คุณยังคงไม่มี None รอบ ๆ ทั้งสำหรับค่าหรือข้อผิดพลาด

ผลลัพธ์ (และทั้งสองอย่าง) ใน F #

จุดเริ่มต้นควรเป็นประเภทผลลัพธ์ของ F #และสหภาพที่ถูกเลือกปฏิบัติ หลังจากทั้งหมดนี้ทำงานบน. NET

ประเภทผลลัพธ์ใน F # คือ:

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

ประเภทตัวเองเท่านั้นดำเนินการสิ่งที่พวกเขาต้องการ

DUs ใน F # อนุญาตให้จับคู่รูปแบบครบถ้วนสมบูรณ์โดยไม่ต้องใช้ค่า 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

เลียนแบบสิ่งนี้ใน C # 8

น่าเสียดายที่ C # 8 ยังไม่มี DUs พวกเขามีกำหนดเวลาสำหรับ C # 9 ใน C # 8 เราสามารถเลียนแบบสิ่งนี้ได้ แต่เราสูญเสียการจับคู่แบบหมดจด:

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

และใช้มัน:

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

หากไม่มีการจับคู่รูปแบบครบถ้วนสมบูรณ์เราจะต้องเพิ่มส่วนเริ่มต้นนั้นเพื่อหลีกเลี่ยงคำเตือนของคอมไพเลอร์

ฉันยังคงมองหาวิธีที่จะได้รับการจับคู่ที่ละเอียดถี่ถ้วนโดยไม่แนะนำค่าตายถึงแม้ว่าพวกเขาจะเป็นเพียงตัวเลือก

ตัวเลือก / อาจจะ

การสร้างคลาส Option โดยวิธีที่ใช้การจับคู่แบบหมดจดนั้นง่ายกว่า:

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

ซึ่งสามารถใช้กับ:

string cateGory = someValue switch { Option<Category> (_    ,false) =>"No Category",
                                     Option<Category> (var v,true)  => v.Name
                                   };
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.