Best way to return multiple results from a function in C# [closed]

  softwareengineering

2

We have function DoStuff. It can return Stuff or Error. What is the cleanest way to do this in C#? I know that it’s best to throw in error case, but it has its own downsides. Also consider case where there are 2 logically different results, which are not errors. what then?

Important for me is that code is readable and robust (i.e. no weird cases when someone can return both success and result by mistake etc.).

Solutions I’ve found so far:

1. Throw an exception

 Stuff DoStuff() { throw new Error(errorCode); }

Pros:

  1. Easy to add new cases
  2. Can pass through multiple layers without altering other code

Cons

  1. Can be swallowed by accident
  2. You need to inspect function body or spec to see how it fails
  3. You need to define additional exception type which is nasty and takes lots of code to do properly

2. Use a container object

class DoStuffResult() {
    DoStuffResult(Stuff success) {...}
    DoStuffResult(Error failure) {...}
    public Success? {get;}
    public Error? {get;}
}


// Consumer
var result = DoStuff();
if (doStuffResult.Success != null) {
   Process(result.Success);
}
else {
   ReportError(result.Error);
}

Pros:

  1. Easy to see what may be there by inspecting container class definition

Cons:

  1. There are both Success and Error properties on single object.
  2. What happens when success returns null? You need enum and complexity increases

3. Pattern matching

interface IDoStuffResult {};
IDoStuffResult DoStuff() {
    if (x) { return new Success(); }
    else { return new Error(); };
}

// customer
DoStuff() switch { 
   Success success -> Process(success),
   Error error -> Report(error),
   _ -> throw new ArgumentOutOfRangeException()
};

Pros:

  1. Cleanest code to read and write

Cons:

  1. Extra marker interface required
  2. More than two possible results visible
  3. It’s not obvious how many possible results are there

Which solution is most readable and easiest to maintain?

7

3

Which solution is most readable and easiest to maintain?

As usual, the one that eliminates the problem in the first place.

For C#, that tends to mean designing the code so that methods only have one (non-exceptional) return type. Sometimes that means a Result type that can be queried. It should not be exceptions that are not exceptional, or implementations of interfaces that violate LSP. Most times though, a simple design change can eliminate the need for this altogether.

1

I like very much your pattern matching approach.


A pattern often used is the Try.. method returning a bool indicating the success and an out parameter returning the result

public bool TryDoStuff(out Stuff result)
{
    do_something();
    if (success) {
        result = some_value;
        return true;
    } else {
        result = default;
        return false;
    }
}

You can use it like this

if (TryDoStruff(out var result)) {
    consume_result();
} else {
    handle_error();
}

You can add more out parameters if required. E.g. if two kinds of result objects are required:

public bool TryDoStuff(out Stuff result, out Error error)
{
    do_something();
    if (success) {
        result = some_value;
        error = default;
        return true;
    } else {
        result = default;
        error = error_object;
        return false;
    }
}

A new possibility arises with the introduction of the value tuples

public (bool success, Stuff result) DoStuff()
{
    do_something();
    if (success) {
        return (true, some_value);
    } else {
        return (false, default);
    }
}

You can use it like this

var (success, result) = DoStuff();

Here also, you can extend it to return two different (or more) objects, where only one will not have the default value (null). The bool value is not necessarily required, as you can test which object is not null. You can also test the result with pattern matching.

11

1

Let’s first consider two well-known extremes.

Option types

An Option type is a type that encapsulates the notion that something may or may not exist. In languages that support Closed Algebraic Sum Types (aka Discriminated Unions aka Tagged Unions aka Variant Types), and Option type can be very simple:

data Option a = None | Some a

That is, an Option of a is either a nullary data constructor representing nothingness or a wrapper around a.

This Option type has a couple of interesting properties: it can be viewed as a collection that has either exactly one element or is empty, i.e. similar to a list of length at-most one. What this means is that you can use all the power of the collections library, and all the knowledge you as a programmer have about collections to work with these Options.

For example: you want to transform the value? Just map (in C# Select) the Option! You want to print it only if it exists? Just foreach it! You have nested computations all of which may or may not return a value and need to use the value of the previous computation? Just flatMap (in C# SelectMany) the Options and the result will either be the result of the entire computation if all of them return a value or None if any of them returns no value. In other words: Options are composable!

Everything you know from how to handle collections transfers directly to Options. Note that in the above examples, there was actually never any point where you would need an if statement or pattern matching or any other kind of conditional to check whether or not the value exists: if you foreach over an empty collection, then simply nothing happens, if you map or flatMap over an empty collection, you get an empty collection back. You can almost treat the Option as if it didn’t exist, and it will just do the right thing.

More generally,Options are not just collections, they are monads as well, meaning you can use all the power of monads. For example, in C#, monads support the LINQ Standard Query Operators and can be used with LINQ Query Expression Syntax. You could write something like this:

var ultimateResult = 
    from res1 in ComputationThatMayOrMayNotReturnAResult()
    from res2 in AnotherComputation(res1)
    from res3 in YetAnotherComputation(res2)
    select res3;

And the result will either be Some(res3) or None.

Option can essentially replace any method that uses the Try… pattern, e.g. int.TryParse(string).

Now, unfortunately, C# does not have Closed Algebraic Sum Types (although there are Github issues about that). But, it is possible to implement Algebraic Sum Types using nothing but simple rank-1 parametric polymorphism (aka Generics) and classical OO inheritance: the type becomes an abstract superclass and the data constructors become concrete subclasses.

In Scala, this looks something like this [Please forgive the large amount of Scala code, I don’t really know C#]:

sealed trait Option[+A] {
  val isPresent: Boolean
  def getOrElse[B >: A](default: => B): B
  def foreach[B](f: A => B): Unit
  def map[B](f: A => B): Option[B]
}

case object None extends Option[Nothing] {
  override final val isPresent = false
  override def getOrElse[B](default: => B) = default
  override def foreach[B](f: Nothing => B) = ()
  override def map[B](f: Nothing => B): this.type = this
}

final case class Some[+A](value: A) extends Option[A] {
  override final val isPresent = true
  override def getOrElse[B >: A](default: => B): value.type = value
  override def foreach[B](f: A => B) = f(value)
  override def map[B](f: A => B): Some[B] = Some(f(value))
}

val twoPlusThree = Some(2).map(_ + 3)
twoPlusThree.foreach(println)
// 5

val ohOh = (None: Option[Int]).map(_ + 3)
ohOh.foreach(println)
// no output, since the `println` is never executed

Scastie link

I chose to show a Scala example first, even though the question is about C#, because it is somewhat easier to see what is going on in the Scala example. (For example, C# does not have upper bounds for generic type parameters, so we have to use A as the parameter type for the default value in getOrElse, which is however a contravariant position, which in turn means that Option cannot be covariant in A, and so on. Also, C# doesn’t have a bottom type (type that is a subtype of all types), so we cannot have a single None object, we need a separate None<T> object for each T, and so on.)

In C#, this could look somewhat like this:

var twoPlusThree = new Some<int>(2).Select(i => i + 3);
twoPlusThree.ForEach(System.Console.WriteLine);
// 5

var ohOh = new None<int>().Select(i => i + 3);
ohOh.ForEach(System.Console.WriteLine);
// no output, since the `WriteLine` is never executed

interface Option<T> /* : IEnumerable<T>, … */ where T : notnull
{
    bool IsPresent { get; }
    T GetOrElse(T defaultValue);
    void ForEach(Action<T> action);
    Option<U> Select<U>(Func<T, U> f) where U : notnull;
}

readonly record struct None<T> : Option<T> where T : notnull
{
    public bool IsPresent { get => false; }
    public T GetOrElse(T defaultValue) => defaultValue;
    public void ForEach(Action<T> action) { }
    public Option<U> Select<U>(Func<T, U> f) where U : notnull => new None<U>();
    // In reality, you would probably have a factory method that caches one `None` for each `U`
}

readonly record struct Some<T>(T value) : Option<T> where T : notnull
{
    public bool IsPresent { get => true; }
    public T GetOrElse(T defaultValue) => value;
    public void ForEach(Action<T> action) => action(value);
    public Option<U> Select<U>(Func<T, U> f) where U : notnull => new Some<U>(f(value));
}

(Obviously, a real-world implementation would have all the IEnumerable methods, also IComparable would be a good idea, and we would implement Serialization and Deconstruction / Pattern Matching, etc.)

As you can see, it is structurally very similar to the Scala code. The main differences are:

  • The Scala version is closed, you cannot add any further subclasses to it. (sealed in Scala means “can only be extended within the same compilation unit”, whereas final means “cannot be extended at all”, i.e. like sealed in C#. Since Option is sealed, it can only be extended within the same compilation unit, and the only two templates that extend it are Some and None, both of which are final.)
  • The Scala version is covariant.
  • The Scala version only has a single None value. (Nothing is a bottom type, i.e. it is a subtype of every type. And since Option is covariant, Option[Nothing] is a subtype of every Option[T] for all T. Therefore, a single None can be used wherever an Option[T] is expected.)

Either types

So, what we had now was a type that encapsulates the notion that something either is there or it isn’t there. In other words, it either is some type or it is nothing.

The other extreme is something that can be either one type or another type. This kind of type is called an Either type. It, too, can be easily expressed as an algebraic sum type:

data Either a b = Left a | Right b

This can be implemented in the same way as Option using inheritance:

sealed trait Either[+A, +B] {
  val isLeft: Boolean
  val isRight: Boolean
  def getLeftOrElse[C >: A](default: => C): C
  def getRightOrElse[C >: B](default: => C): C
  def either[C](f: A => C)(g: B => C): C
}

final case class Left[+A](value: A) extends Either[A, Nothing] {
  override final val isLeft = true
  override final val isRight = false
  override def getLeftOrElse[C >: A](default: => C): value.type = value
  override def getRightOrElse[C](default: => C) = default
  override def either[C](f: A => C)(g: Nothing => C) = f(value)
}

final case class Right[+B](value: B) extends Either[Nothing, B] {
  override final val isLeft = false
  override final val isRight = true
  override def getLeftOrElse[C](default: => C) = default
  override def getRightOrElse[C >: B](default: => C): value.type = value
  override def either[C](f: Nothing => C)(g: B => C) = g(value)
}

val s: Either[String, Int] = Left("Hello")
val i: Either[String, Int] = Right(23)

println(s.either(_.length)(_ * 2))
// 5
println(i.either(_.length)(_ * 2))
// 46

Scastie link

Scala has some other syntactic niceties that we can use. For example, generic types with exactly two type parameters can be written A Foo B instead of Foo[A, B]. So, if we rename our Either to Or, we could write A Or B as the type, which reads really nice. In fact, | is a legal identifier like any other in Scala, so we could also name the trait | and write A | B.

In C#, it would look roughly like this:

Either<string, int> s = new Left<string, int>("Hello");
Either<string, int> i = new Right<string, int>(23);

System.Console.WriteLine(s.Either(x => x.Length, x => x * 2));
// 5
System.Console.WriteLine(i.Either(x => x.Length, x => x * 2));
// 46

interface Either<A, B> where A : notnull where B : notnull
{
    bool IsLeft { get; }
    bool IsRight { get; }
    A GetLeftOrElse(A defaultValue);
    B GetRightOrElse(B defaultValue);
    C Either<C>(Func<A, C> f, Func<B, C> g) where C : notnull;
}

readonly record struct Left<A, B>(A value) : Either<A, B> where A : notnull where B : notnull
{
    public bool IsLeft { get => true; }
    public bool IsRight { get => false; }
    public A GetLeftOrElse(A defaultValue) => value;
    public B GetRightOrElse(B defaultValue) => defaultValue;
    public C Either<C>(Func<A, C> f, Func<B, C> g) where C : notnull => f(value);
}

readonly record struct Right<A, B>(B value) : Either<A, B> where A : notnull where B : notnull
{
    public bool IsLeft { get => false; }
    public bool IsRight { get => true; }
    public A GetLeftOrElse(A defaultValue) => defaultValue;
    public B GetRightOrElse(B defaultValue) => value;
    public C Either<C>(Func<A, C> f, Func<B, C> g) where C : notnull => g(value);
}

(Obviously, a real-world implementation would implement Serialization and Deconstruction / Pattern Matching, etc.)

Unfortunately, the Either type does not have the nice monadic properties of Option. (Unless you “bias” it to one side, that is.) So, we can’t implement IEnumerable, Select, SelectMany, ForEach, and friends. We have to either implement everything twice, or always pass both alternatives. So, for example instead of ForEach we would have ExecuteIfLeft(Action<A>) and ExecuteIfRight(Action<B>), or ExecuteEither(Action<A>, Action<B>), and similar for Select, and so on.

This is essentially a Discriminated Union implemented in a language that doesn’t support them.

Error types

Halfway in between Option (which only supports one type or the absence of a value) and Either (which completely generically supports two types) lies the Error type, which supports either a value or an error. You can think of it as a biased Either, where one type is fixed to be an error type (type Error[A] = Either[A, Throwable]). Because it is biased to one side, it satisfies all the monadic properties and can implement the LINQ Standard Query Operators (meaning Select, SelectMany, ForEach, etc. all prefer one side).

The semantics are similar to Option: it either returns the result of the whole computation chain, or it returns the first error and aborts the chain.

import scala.util.control.NonFatal

sealed trait Try[+A] {
  val isSuccess: Boolean
  val isFailure: Boolean
  def getOrElse[B >: A](default: => B): B
  def foreach[B](f: A => B): Unit
  def map[B](f: A => B): Try[B]
  def flatMap[B](f: A => Try[B]): Try[B]
}

final case class Success[+A](value: A) extends Try[A] {
  override final val isSuccess = true
  override final val isFailure = false
  override def getOrElse[B >: A](default: => B): value.type = value
  override def foreach[B](f: A => B) = f(value)
  override def map[B](f: A => B): Success[B] = Success(f(value))
  override def flatMap[B](f: A => Try[B]) = try f(value) catch { case NonFatal(e) => Failure(e) }
}

final case class Failure[+A, E <: Throwable](ex: E) extends Try[A] {
  override final val isSuccess = false
  override final val isFailure = true
  override def getOrElse[B](default: => B) = default
  override def foreach[B](f: A => B) = ()
  override def map[B](f: A => B) = this.asInstanceOf[Try[B]]
  override def flatMap[B](f: A => Try[B]) = this.asInstanceOf[Try[B]]
}

object Try {
  def apply[A](f: => A): Try[A] =
    try Success(f) catch {
      case NonFatal(e) => Failure(e)
    }
}

Scastie link

And again in C#

interface Try<T> where T : notnull
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    T GetOrElse(T defaultValue);
    void ForEach(Action<T> action);
    Try<U> Select<U>(Func<T, U> f) where U : notnull;
}

readonly record struct Success<T>(T value) : Try<T> where T : notnull
{
    public bool IsSuccess { get => true; }
    public bool IsFailure { get => false; }
    public T GetOrElse(T defaultValue) => value;
    public void ForEach(Action<T> action) => action(value);
    public Try<U> Select<U>(Func<T, U> f) where U : notnull => new Success<U>(f(value));
}

readonly record struct Failure<T, E>(E ex) : Try<T> where T : notnull where E : Exception
{
    public bool IsSuccess { get => false; }
    public bool IsFailure { get => true; }
    public T GetOrElse(T defaultValue) => defaultValue;
    public void ForEach(Action<T> action) { }
    public Try<U> Select<U>(Func<T, U> f) where U : notnull => this as Try<U>;
}

I believe between these three examples, we have covered all of your use cases (two different types, a type or an error) as well as another common case (value that might exist or not).

In all three cases, we can use pattern matching, as you suggested. But actually, especially for Option and Try, it is even better to treat them either as collections (using ForEach, Select, SelectMany, Where, and so on) or as monads (in LINQ Query Expressions), because they just do the right thing by themselves without the need for any sort of conditional (whether that be an if statement or pattern matching).

All three of these are essentially your proposal #2, while also facilitating #3, but with well-known names and reusable, general semantics. In fact, many languages ship these three types in their standard libraries, e.g. Scala: Option, Either, Try. Even Java now ships with Optional, although unfortunately, this one does not implement the collection API.

1

-1

C# also has structures which are arguably simpler than classes, since they are copied more or less directly by value rather than allocated (presumably in the heap) and passed by reference.

C# also has several types included in the library, namely Tuple<type1,type2> (and Tuple<type1,type2,type3>, etc..), which means you can declare a return type as a struct containing two members without even having to declare a new or custom struct type, if you are willing to settle for non-descript field names like Item1 and Item2.

Newer C# also has simpler tuple syntax and destructuring assignment.

5

LEAVE A COMMENT