C # не может сделать тип `notnull` обнуляемым

9

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

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

?


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

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

Ответы:

12

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

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

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

Это неверно по той же причине.

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

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

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

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

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

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

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

Джон Скит
источник
Также есть магия времени выполнения - вы не можете сделать nullable nullable, даже если нет способа представить это ограничение в IL. Nullable<T>это особый тип, который вы не можете сделать сами. И еще есть бонусный момент, как бокс делается с нулевыми типами.
Луаан
1
@Luaan: есть магия времени выполнения для типов значений Nullable, но не для типов ссылок NULL.
Джон Скит
6

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

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

  • Один и тот же тип должен содержать мертвое значение, либо тип, либо ошибку, либо возвращать нули
  • Сопоставление с образцом по типу невозможно. Вы должны будете использовать некоторые причудливые выражения соответствия позиционных шаблонов, чтобы заставить это работать.
  • Чтобы избежать пустых значений, вам нужно использовать что-то вроде Option / Maybe, похожее на F #'s Options . Тем не менее, вы все равно носите 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 с помощью исчерпывающего сопоставления проще:

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
                                   };
Панагиотис Канавос
источник