Обновление
Если вы сделали изменили использование и преобразовали IsSuccess
в свойство, вы могли бы избавиться от проблем обнуляемости и получить исчерпывающее соответствие. Это выражение переключения является исчерпывающим ie, компилятор может проверить, что все возможности были выполнены. Требуется, чтобы каждая ветвь только получала действительное свойство:
var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
Если ваши методы принимают и возвращают IResult<T>
объекты, вы можете написать что-то вроде:
IResult<string> Doubler(IResult<string> input)
{
return input switch { {IsSuccess:true,Data:var data} => new Ok<string>(data+ "2"),
{IsSuccess:false} => input
};
}
...
var result2=new Ok<string>("3");
var message2=Doubler(result2) switch {
{IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
Оригинальный ответ
Похоже, проблема real заключается в реализации шаблона Result . Этот шаблон имеет две характеристики:
- Он предотвращает использование недопустимых значений результата на уровне типа. Он делает это, используя два разных типа для представления хороших и плохих результатов. При этом каждый тип несет только то, что ему нужно.
- Это заставляет клиентов обрабатывать все случаи или явно игнорировать их.
Некоторые языки, такие как Rust, имеют встроенный тип для этого. Функциональные языки, которые поддерживают типы опций / различимые объединения, такие как F #, могут легко реализовать это с помощью:
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Исчерпывающее сопоставление с образцом означает, что клиенты должны обрабатывать оба случая. Этот тип настолько распространен, что превратил его в сам язык.
C# 8
В C# 8 мы можем реализовать два типа без исчерпывающего сопоставления с образцом. На данный момент типам необходим общий класс, либо интерфейс, либо абстрактный класс, который на самом деле не должен иметь никаких членов. Есть много способов их реализации, например:
public interface IResult<TSuccess,TError>{}
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
public TSuccess Data{get;}
public Ok(TSuccess data)=>Data=data;
public void Deconstruct(out TSuccess data)=>data=Data;
}
public class Fail<TSuccess,TError>:IResult<TSuccess,TError>
{
public TError Error{get;}
public Fail(TError error)=>Error=error;
public void Deconstruct(out TError error)=>error=Error;
}
Мы могли бы использовать структуры вместо классов.
Или, если использовать синтаксис, близкий к разграниченным C# 9 объединениям, классы могут быть вложенными. Тип по-прежнему может быть интерфейсом, но мне действительно не нравится писать new IResult<string,string>.Fail
или называть интерфейс Result
вместо IResult
:
public abstract class Result<TSuccess,TError>
{
public class Ok:Result<TSuccess,TError>
{
public TSuccess Data{get;}
public Ok(TSuccess data)=>Data=data;
public void Deconstruct(out TSuccess data)=>data=Data;
}
public class Fail:Result<TSuccess,TError>
{
public TError Error{get;}
public Fail(TError error)=>Error=error;
public void Deconstruct(out TError error)=>error=Error;
}
//Convenience methods
public static Result<TSuccess,TError> Good(TSuccess data)=>new Ok(data);
public static Result<TSuccess,TError> Bad(TError error)=>new Fail(error);
}
Мы можем использовать сопоставление с шаблоном для обработки Result
ценности. К сожалению, C# 8 не предлагает исчерпывающего соответствия, поэтому нам нужно добавить регистр по умолчанию.
var result=Result<string,string>.Bad("moo");
var message=result switch { Result<string,string>.Ok (var Data) => $"Got some: {Data}",
Result<string,string>.Fail (var Error) => $"Oops {Error}"
_ => throw new InvalidOperationException("Unexpected result case")
};
C# 9
C# 9 (вероятно) собирается добавить различимые объединения через перечислимые классы . Мы сможем написать:
enum class Result
{
Ok(MySuccess Data),
Fail(MyError Error)
}
и использовать его при сопоставлении с образцом. Этот синтаксис уже работает в C# 8, пока есть соответствующий деконструктор. C# 9 добавит исчерпывающее сопоставление и, возможно, также упростит синтаксис:
var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
Result.Fail (var Error) => $"Oops {Error}"
};
Обновление существующего типа с помощью DIM
Некоторые из существующих функций, такие как IsSuccess
и Outcome
- это просто удобные методы. Фактически, типы опций F # также представляют «вид» значения как тег . Мы можем добавить такие методы в интерфейс и вернуть фиксированное значение из реализаций:
public interface IResult<TSuccess,TError>
{
public bool IsSuccess {get;}
public bool IsFailure {get;}
public bool ResultOutcome {get;}
}
public class Ok<TSuccess,string>:IResult<TSuccess,TError>
{
public bool IsSuccess =>true;
public bool IsFailure =>false;
public bool ResultOutcome =>ResultOutcome.Success;
...
}
Свойства Description
и Data
также могут быть реализованы как мера остановки пробела - они нарушают Результат сопоставление с образцом и шаблоном делает их в любом случае устаревшими:
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
...
public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
...
}
Члены интерфейса по умолчанию могут использоваться для предотвращения засорения конкретных типов:
public interface IResult<TSuccess,TError>
{
//Migration methods
public TSuccess Data=>
(this is Ok<TSuccess,TError> (var Data))
?Data
:throw new InvalidOperationException("An Error has no data");
public TError Description=>
(this is Fail<TSuccess,TError> (var Error))
?Error
:throw new InvalidOperationException("A Success Result has no Description");
//Convenience methods
public static IResult<TSuccess,TError> Good(TSuccess data)=>new Ok<TSuccess,TError>(data);
public static IResult<TSuccess,TError> Bad(TError error)=>new Fail<TSuccess,TError>(error);
}
Модификации для добавления исчерпывающего сопоставления
Мы могли бы избежать случаев по умолчанию в исключениях сопоставления с образцом, если бы использовали только один флаг и свойства миграции:
public interface IResult<TSuccess,TError>
{
public bool IsSuccess{get;}
public bool IsFailure=>!IsSuccess;
//Migration methods
...
}
var message2=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
На этот раз компилятор обнаруживает Есть только два случая, и оба охвачены. Свойства миграции позволяют компилятору получать правильный тип. Код потребления должен изменить и , используя правильный шаблон, но я подозреваю, что это уже сработало, что