C # не может сделать тип notnull допускающим значение NULL

Я пытаюсь создать тип, похожий на Result в Rust или Either в Haskell, и я дошел до этого:

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, почему он жалуется (везде, где есть параметр типа со знаком обнуляемого ? после него), что:

Параметр типа, допускающий значение NULL, должен быть известен как тип значения или ссылочный тип, не допускающий значения NULL. Рассмотрите возможность добавления ограничения типа, структуры или типа.

?


Я использую C # 8 в .NET Core 3 с включенными ссылочными типами, допускающими значение NULL.


person Shoe Diamente    schedule 14.11.2019    source источник
comment
Вместо этого вы должны начать с типа результата F # и размеченных объединений. Вы можете легко добиться чего-то подобного в C # 8, без того, чтобы нести мертвую ценность, но у вас не будет исчерпывающего сопоставления. Попытка поместить оба типа в одну структуру приведет к одной проблеме за другой и вернет те самые проблемы. Результат должен был исправить   -  person Panagiotis Kanavos    schedule 14.11.2019


Ответы (2)


По сути, вы просите то, что невозможно представить в IL. Типы значений, допускающие значение NULL, и ссылочные типы, допускающие значение NULL, - очень разные звери, и хотя они выглядят одинаково в исходном коде, IL сильно отличается. Версия с нулевым значением типа значения T - это другой тип (Nullable<T>), тогда как версия с нулевым значением ссылочного типа T - это тот же тип, с атрибутами, указывающими компилятору, чего ожидать.

Рассмотрим этот более простой пример:

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

Это недействительно по той же причине.

Если мы ограничим T структурой, тогда IL, сгенерированный для метода GetNullValue, будет иметь тип возврата Nullable<T>.

Если мы ограничим T ссылочным типом, не допускающим значения NULL, тогда IL, сгенерированный для метода GetNullValue, будет иметь тип возврата T, но с атрибутом для аспекта допустимости NULL.

Компилятор не может сгенерировать IL для метода, который имеет тип возвращаемого значения T и Nullable<T> одновременно.

Это в основном результат того, что ссылочные типы, допускающие значение NULL, вообще не являются концепцией CLR - это просто волшебство компилятора, которое помогает вам выражать намерения в коде и заставляет компилятор выполнять некоторую проверку во время компиляции.

Сообщение об ошибке не так однозначно, как могло бы быть. T известен как «тип значения или ссылочный тип, не допускающий значения NULL». Более точное (но значительно более многословное) сообщение об ошибке будет:

Параметр типа, допускающий значение NULL, должен быть известен как тип значения или как ссылочный тип, не допускающий значения NULL. Рассмотрите возможность добавления ограничения типа, структуры или типа.

В этот момент ошибка будет разумно относиться к нашему коду - параметр типа «не известен как тип значения» и не «известен как ссылочный тип, не допускающий значения NULL». Известно, что это один из двух, но компилятор должен знать, какой.

person Jon Skeet    schedule 14.11.2019
comment
Также есть магия времени выполнения - вы не можете сделать nullable nullable, даже если нет способа представить это ограничение в IL. Nullable<T> - особый тип, который вы не можете сделать самостоятельно. И еще есть бонус в том, как бокс выполняется с нулевыми типами. - person Luaan; 14.11.2019
comment
@Luaan: есть магия времени выполнения для типов значений, допускающих значение NULL, но не для ссылочных типов, допускающих значение NULL. - person Jon Skeet; 14.11.2019

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

Более глубокая проблема заключается в том, что использование одного типа для реализации Result и удержания значений Success и Error возвращает те же проблемы, которые должен был исправить Результат, и несколько других.

  • Один и тот же тип должен нести мертвое значение, либо тип, либо ошибку, либо возвращать нули.
  • Сопоставление с образцом для типа невозможно. Чтобы это сработало, вам придется использовать несколько причудливых выражений сопоставления с позиционным образцом.
  • Чтобы избежать нулей, вам придется использовать что-то вроде Option / Maybe, похожее на Параметры. Тем не менее, вы все равно будете носить с собой None либо для значения, либо для ошибки.

Результат (и любой) в 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.

Вариант / Возможно

Создание класса 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
                                   };
person Panagiotis Kanavos    schedule 14.11.2019