I have a IResult<T>
container that I use to handle errors. It looks like this:
public interface IResult<out T>
{
ResultOutcome Outcome { get; } //enum: {Failure, Uncertain, Success}
string Description { get; } //string describing the error, in case of !Success
bool IsSuccess(); //Outcome == Success
T Data { get; } //If success, it contains the data passed on, otherwise NULL
}
And you'd use it like this:
IResult<int> GetSomething()
{
try{
int result = //things that might throw...
return Result<int>.Success(result);
}
catch(Exception e)
{
return Result<int>.Failure($"Something went wrong: {e.Message}");
}
}
And then:
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- no errors, so there is something in here.
Until now, all good. When I introduce the nullable types, though, I have a problem:
public interface IResult<out T> where T : class // unfortunately this is necessary
{
...
T? Data { get; } //If success, it contains the data passed on, otherwise NULL
}
var result = GetSomething();
if (!result.IsSuccess()) return result; //<- error passed on.
int resultData = result.Data; //<- WARNING!!! POSSIBLE DEREFERENCE OF NULL
Now the question: I am sure that result.Data
contains something, as it passed the IsSuccess()
step. How can I reassure the compiler about it? Is there a way or C#8 nullable concept is just not compatible with this?
Are there other ways to handle results in a similar fashion? (passing on containers instead of exceptions).
P.s. 1
Please, don't suggest to use result.Data!;
.
P.s. 2
This code is already used on a thousand of lines or more, so if the change can be on the interface, rather than on the usages, it would be way better.
C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...
What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.
In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr.
Compared to other languages—like Java, PHP, or C#—C is a relatively simple language to learn for anyone just starting to learn computer programming because of its limited number of keywords.
Update
If you did change the usage, and converted IsSuccess
to a property, you could get rid of nullability issues and get exhaustive matching. This switch expression, is exhaustive ie the compiler can check that all possibilities have been met. It does require that each branch only retrieves a valid property though :
var message=result switch { {IsSuccess:true,Data:var data} => $"Got some: {data}",
{IsSuccess:false,Description:var error} => $"Oops {error}",
};
If your methods accept and return IResult<T>
objects, you could write something like :
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}",
};
Original Answer
It looks like the real problem is the implementation of the Result pattern. This pattern has two characteristics :
Some languages like Rust have a built-in type for this. Functional languages that support option types/discriminated unions like F#, can easily implement it with just :
type Result<'T,'TError> =
| Ok of ResultValue:'T
| Error of ErrorValue:'TError
Exhaustive pattern matching means clients have to handle both cases. That type is so common though, it made it into the language itself.
C# 8
In C# 8 we can implement the two types, without the exhaustive pattern matching. For now, the types need a common class, either an interface or abstract class, which doesn't really need to have any members. There are many ways to implement them, eg :
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;
}
We could use structs instead of classes.
Or, to use a syntax closer to C# 9's discriminated unions, the classes can be nested. The type can still be an interface, but I really don't like writing new IResult<string,string>.Fail
or naming an interface Result
instead of 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);
}
We can use pattern matching to handle Result
values. Unfortunatelly, C# 8 doesn't offer exhaustive matching so we need to add a default case too.
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 is (probably) going to add discriminated unions through enum classes. We'll be able to write :
enum class Result
{
Ok(MySuccess Data),
Fail(MyError Error)
}
and use it through pattern matching. This syntax already works in C# 8 as long as there's a matching deconstructor. C# 9 will add exhaustive matching and probably simplify the syntax too :
var message=result switch { Result.Ok (var Data) => $"Got some: {Data}",
Result.Fail (var Error) => $"Oops {Error}"
};
Updating the existing type through DIMs
Some of the existing functions like IsSuccess
and Outcome
are just convenience methods. In fact, F#'s option types also expose the "kind" of the value as a tag . We can add such methods to the interface and return a fixed value from the implementations :
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;
...
}
The Description
and Data
properties can be implemented too, as a stop gap measure - they break the Result pattern and pattern matching makes them obsolete anyway :
public class Ok<TSuccess,TError>:IResult<TSuccess,TError>
{
...
public TError Description=>throw new InvalidOperationException("A Success Result has no Description");
...
}
Default interface members can be used to avoid littering the concrete types :
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);
}
Modifications to add exhaustive matching
We could avoid the default cases in the pattern matching exceptions if we use only one flag and the migration properties :
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}",
};
This time, the compiler detects there are only two cases, and both are covered. The migration properties allow the compiler to retrieve the correct type. The consuming code has to change and use the correct pattern, but I suspect it already worked that
With c# 9 there is MemberNotNullWhen
attribute which can hide corresponding warning when IsSuccess
is checked
public interface IResult<out T>
{
[MemberNotNullWhen(true, nameof(Data))]
bool IsSuccess();
T? Data { get; }
}
IResult<string> res = GetSomeResult();
if(!res.IsSuccess())
throw new Exception(); // or just return something else
var len = res.Data.Length; // no nullability warning
The official microsoft docs are not updated yet. I received more nullability information about it within this lection. In order to use the above attribute, you must use .net50, or set language version within csproj file as c#9. Another way to backport these attributes is to use Nullable package.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With