C # 's не може зробити тип `notnull` нульовим


9

Я намагаюся створити тип, схожий на Руста Resultабо Хаскелла, 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, чому він скаржиться (де завгодно, де є параметр типу зі ?знаком, що зводиться після нього), що:

Параметр типу нульового типу повинен бути відомий як тип значення або нерегульований опорний тип. Подумайте про додавання обмежень "клас", "структура" або типу.

?


Я використовую C # 8 у .NET Core 3 з увімкненими нульовими посиланнями.


Натомість слід починати з типу результатів F # та дискримінаційних об'єднань. Ви легко можете досягти чогось подібного в C # 8, не несучи мертвого значення навколо, але вичерпної відповідності не будете. Спроба ввести обидва типи в одну структуру зіткнеться з однією проблемою за іншою, і поверне ті самі проблеми, які повинен був виправити результат
Панайотис Канавос

Відповіді:


12

В основному ви просите щось, що не може бути представлене в IL. Типи нульового значення та нульові еталонні типи є дуже різними звірами, і хоча вони схожі у вихідному коді, ІЛ дуже відрізняється. Версія, що зводить нульовий тип, Tє іншим типом ( Nullable<T>), тоді як нульова версія еталонного типу Tє тим самим типом, атрибути говорять компілятору, чого очікувати.

Розглянемо цей простіший приклад:

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

Це недійсно з тієї ж причини.

Якщо ми обмежуємося Tструктурою, то ІЛ, сформований для GetNullValueметоду, мав би тип повернення Nullable<T>.

Якщо ми обмежуємося нетильовим Tеталонним типом, то IL, згенерований для GetNullValueметоду, мав би тип повернення T, але з атрибутом для аспекту зведеності.

Компілятор не може генерувати IL для методу , який має тип повернення як Tі Nullable<T>в той же самий час.

Це, в основному, результат змінних типів посилань, які взагалі не є концепцією CLR - це просто магія компілятора, яка допоможе вам висловити наміри в коді і змусити компілятора виконати деяку перевірку під час компіляції.

Повідомлення про помилку не настільки чітке, як може бути. Tвідомо, що "тип значення або нерегульований тип посилання". Більш точним (але значно важчим) повідомлення про помилку буде:

Параметр типу нульового типу повинен бути відомий як тип значення або відомий як нерегульований опорний тип. Подумайте про додавання обмежень "клас", "структура" або типу.

У цей момент помилка може бути застосована до нашого коду - параметр типу не є "відомим типом значення" і не "відомо, що це нерегульований опорний тип". Відомо, що це одне з двох, але компілятору потрібно знати, який .


Існує і магія виконання - ви не можете зробити нульовим нульовим, навіть якщо немає можливості представити це обмеження в IL. Nullable<T>це особливий тип, який ви не можете зробити самостійно. А потім є бонусний бал того, як робиться бокс з нульовими типами.
Луань

1
@Luaan: Існує магія виконання для типів змінних значень, але не для змінних типів посилань.
Джон Скіт

6

Причина попередження пояснюється в розділі The issue with T?" Випробуйте Nullable посилальних типів . Короткий опис короткого оповідання, якщо ви користуєтеся, T?вам слід вказати, чи є тип класом чи структурою Ви можете створити два типи для кожного випадку.

Більш глибока проблема полягає в тому, що використання одного типу для реалізації Result і утримування значень Success і Error повертає ті самі проблеми, які Result повинен був виправити, та ще кілька.

  • Один і той же тип повинен нести мертве значення навколо типу або помилки або повертати нулі
  • Узгодження шаблону за типом неможливо. Вам потрібно буде використовувати деякі вигадливі вирази відповідності позиційного шаблону, щоб дозволити цьому працювати.
  • Щоб уникнути нулів, вам доведеться використовувати щось на зразок Опція / Можливо, схоже на Параметри F # . Ви все одно несете "Нічого" навколо значення або помилки.

Результат (і в будь-якому випадку) у F #

Початковою точкою повинен бути тип результату F # та дискриміновані спілки. Зрештою, це вже працює на .NET.

Тип результату в F # є:

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

Самі типи несуть лише те, що їм потрібно.

DU в F # дозволяють вичерпне узгодження шаблону, не вимагаючи нулів:

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 ще немає DU, вони заплановані на 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
                                   };
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.