Обнуляемые ссылочные типы и любой шаблон - PullRequest
4 голосов
/ 24 сентября 2019

Я пробую новые Обнуляемые ссылочные типы в C # 8.0 , и я сталкиваюсь с этой проблемой.Учитывая эту структуру

public readonly struct Either<TReturn, TError>
    where TReturn : struct
    where TError : struct
{
    public TError? Error { get; }
    public TReturn? Response { get; }

    public Either(TError? error, TReturn? response)
    {
        if (error == null && response == null)
        {
            throw new ArgumentException("One argument needs not to be null.");
        }
        if (error != null && response != null)
        {
            throw new ArgumentException("One argument must be null.");
        }
        Error = error;
        Response = response;
    }
}

Как я могу сказать компилятору, что Error или Response не равно нулю, и что они не могут быть оба равны нулю?Есть ли способ сделать это с новыми атрибутами?

Ответы [ 3 ]

3 голосов
/ 24 сентября 2019

Обновление для структур

Код не изменяется при изменении типов результатов на структуры.Чтобы использовать параметры типа структуры, необходимо добавить следующие ограничения в интерфейс и типы:

where TResult : struct
where TError  : struct

Когда я думаю о шаблоне Either, я думаю о F #, сопоставлении с образцом и различимых объединениях, не нули.Фактически, Either - это способ избежать нулей.На самом деле код вопроса выглядит как попытка создать Result type , а не просто Either. Скотт Влашин * Железнодорожно-ориентированное программирование показывает, как такой тип может использоваться для реализации обработки ошибок на функциональном языке.

В F # тип Result определяется как:

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

Мы еще не можем сделать это в C # 8, потому что нет дискриминационных союзов.Они запланированы для C # 9.

Сопоставление с образцом

Что мы можем сделать, это использовать сопоставление с образцом, чтобы получить то же поведение, например:

interface IResult<TResult,TError>{} //No need for an actual implementation

public class Success<TResult,TError>:IResult<TResult,TError>

{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}
}

Таким образом, нет способа создать IResult<>, который будет и успешным, и ошибочным.Это можно использовать с сопоставлением с образцом, например:

IResult<int,string> someResult=.....;

if(someResult is Success<int,string> s)
{
    //Use s.Result here
}

Упрощение выражений

Учитывая паттерны свойств C # 8 , это можно переписатькак:

if(someResult is Success<int,string> {Result: var result} )
{
    Console.WriteLine(result);
}

или, используя выражения-переключатели, типичный железнодорожный вызов:

IResult<int,string> DoubleIt(IResult<int,string> data)
{
    return data switch {    Error<int,string> e=>e,
                            Success<int,string> {Result: var result}=>
                                       new Success<int,string>(result*2),
                            _ => throw new Exception("Unexpected type!")
                            };
}    

F # не понадобится throw, поскольку * 1051 не существует* будет чем-то отличным от Ok или Error.В C # у нас пока нет этой функции .

Выражение-переключатель допускает исчерпывающее сопоставление.Я думаю, что компилятор выдаст предупреждение, если предложение по умолчанию также отсутствует.

С деконструкторами

Выражения могут быть упрощены немного больше, если типы имеют деконструкторы,Например:

public class Success<TResult,TError>:IResult<TResult,TError>
{
    public TResult Result {get;}

    public Success(TResult result) { Result=result;}

    public void Deconstruct(out TResult result) { result=Result;}
}

public class Error<TResult,TError>:IResult<TResult,TError>
{
    public TError ErrorValue {get;}

    public Error(TError error) { ErrorValue=error;}

    public void Deconstruct(out TError error) { error=ErrorValue;}
}

В этом случае выражение может быть записано как:

return data switch {    
                Error<int,string> e => e,
                Success<int,string> (var result) => new Success<int,string>(result*3),
                _ => throw new Exception("Unexpected type!")
};

Обнуляемость

Вопрос начался с обнуляемых ссылочных типовтак что насчет обнуляемости?Получим ли мы предупреждение в C # 8, если попытаемся передать значение nulll?

Да, если включены NRT.Этот код:

#nullable enable

void Main()
{
     IResult<string,string> data=new Success<string,string>(null);
     var it=Append1(data);
     Console.WriteLine(it);
}

IResult<string,string> Append1(IResult<string,string> data)
{
    return data switch {    Error<string,string> e=>e,
                            Success<string,string> (var result)=>
                                new Success<string,string>(result+"1"),
                            _ => throw new Exception("Unexpected type!")
                            };
}

Генерирует CS8625: Cannot convert null literal to non-nullable reference type

Пытается

string? s=null;
IResult<string,string> data=new Success<string,string>(s);

Генерирует CS8604: Possible null reference argument ....

2 голосов
/ 24 сентября 2019

При реализации монады Either вы должны использовать два разных конструктора.Таким образом, вы можете легко избежать этих проверок, потому что ваша реализация будет следить за тем, чтобы оба свойства не были назначены одновременно.

public readonly class Either<TReturn, TError>
{
    bool _successful;
    private TError _error { get; }
    private TReturn _response { get; }

    public Either(TError error)
    {
        _error = error;
    }

    public Either(TReturn response)
    {
       _successful = true;
       _response = response;
    }
}

Кроме того, вам нужно добавить метод(в структуру), которая будет использоваться для извлечения значения из структуры и преобразования его в общий тип возвращаемого значения:

public Match<T>(Func<TError, T> errorFunc, Func<TResponse, T> successFunc)
    => _successful ? successFunc(_response) : errorFunc(_error);

Таким образом, вы заставляете пользователей обрабатывать оба случая (успех, ошибка)и предоставить функции, которые будут преобразовывать в общий тип:

var errorEither = new Either<string, int>(10); // example of error code
var successEither = new Either<string, int>("success"); // example of success

var commonValueError = errorEither.Match<bool>(err => false, succ => true);
var commonValueSuccess = successEither.Match<bool>(err => false, succ => true);
0 голосов
/ 24 сентября 2019

Вы можете сделать что-то подобное с Resharper ContractAnnotation.Это не специфично для C # 8, (но ... я не думаю, что ваш пример действительно использует обнуляемые ссылочные типы в любом случае, верно? Вы используете обнуляемые структуры.)

[ContractAnnotation(
   "=> pError: null, pResponse: notnull; => pError: notnull, pResponse: null"
)]
public void Get(out TError? pError, out TReturn? pResponse) {
   pError = Error;
   pResponse = Response;
}

(Значениестрока означает, что слева от => указаны условия ввода, а справа от => указаны условия вывода, при этом ; разделяет различные случаи, а значение без метки относится к возвращаемому значению метода. Так что в этом случае: независимо отinput, условие вывода - эфир null / notnull или notnull / null.)

Затем используйте функцию C # 7 out var:

GetAnEitherFromSomewhere()
.Get(out var error, out var response);
if (error != null) {
   // handle error
   return;
}

// response is now known to be not null, because we can only get here if error was null

Честно говоря, я нахожу аннотации JetBrains [NotNull], [CanBeNull] и [ContractAnnotation] должны быть намного более гибкими (хотя и более многословными), чем ссылочные типы, допускающие значение NULL.В основном они учитывают промежуточный случай, когда существуют ситуации, когда значение может быть null, но также и ситуации, когда значение не может быть null, и эти ситуации различимы во время выполнения.При использовании ссылочных типов, допускающих обнуляемость, я не могу указать промежуточный регистр, я должен выбрать либо однозначно обнуляемый, либо определенно не обнуляемый.

Даже что-то общее, например TryParse:

// should the result be nullable with nullable reference types on?
// you have to either lie with ! or else use ? and assume it can always be null
public bool TryParse(string pString, out SomeClass pResult) {
   if (<cant parse>) {
      pResult = null;
      return false;
   }

   pResult = value;
   return true;
}

// works great with JetBrains annotations and nullable reference types off
// now you can know that the result is null or notnull
// based on testing the bool return value
[ContractAnnotation("=> true, pResult: notnull; => false, pResult: null")]
public bool TryParse(string pString, out SomeClass pResult) {
   ...
}
...