C# Return One Of Two Types

9 min read

In C#, returning one of two types is a common requirement when you need a method to produce different kinds of results based on certain conditions. In real terms, this can be achieved using several approaches, each with its own strengths and use cases. Understanding these methods allows you to write cleaner, more maintainable code while keeping type safety intact.

One of the most straightforward ways to return different types is by using inheritance and polymorphism. If you have two or more types that share a common base class or interface, you can declare the method's return type as that base or interface. This way, the method can return any object that derives from it. Take this: if you have a base class Shape and derived classes Circle and Rectangle, a method can return either type by declaring its return type as Shape. This approach is powerful but requires a well-designed class hierarchy and may not always be applicable if the types do not share a common ancestor.

Another approach is to use the object type as the return type. Since all types in C# ultimately derive from object, a method can return any type by declaring its return type as object. This can lead to runtime errors if the cast is incorrect. On the flip side, this method sacrifices type safety, as you must cast the returned value back to its original type before use. While this method is flexible, it is generally discouraged in favor of more type-safe alternatives.

The dynamic type offers another option for returning different types. Even so, by declaring a method's return type as dynamic, you can return any type, and the type checking is deferred until runtime. This provides flexibility but at the cost of compile-time type safety, making it more error-prone. It is best used when you have no other choice or when working with dynamic languages or data sources Turns out it matters..

For scenarios where you need to return one of two specific types and want to maintain type safety, C# provides the System.ValueTuple and System.Tuple types. These allow you to return multiple values from a method, each potentially of a different type. Still, for example, a method could return a ValueTuple<object, object> and the caller can check which value is not null to determine the result. While this approach works, it can be less elegant and more cumbersome than other options.

A more modern and type-safe solution is to use generics. Plus, by defining a method with a generic type parameter, you can return one of two types while preserving type information. To give you an idea, you can create a method that returns a T or a U, where T and U are type parameters. This approach is flexible and type-safe, but it requires the caller to know which type to expect or to use additional logic to handle both possibilities.

One of the most reliable solutions for returning one of two types is to use a discriminated union, which can be implemented using a struct or a class with a private constructor and static factory methods. This pattern ensures that only one of the two possible types is present at any time, and the caller can use pattern matching or type checking to determine which type was returned. This approach provides strong type safety and clear intent, making the code more maintainable and less error-prone Practical, not theoretical..

Take this: you can define a Result<TSuccess, TError> type that can hold either a success value of type TSuccess or an error value of type TError. The type exposes methods to check which value is present and to retrieve it safely. This pattern is widely used in functional programming and is becoming more popular in C# for handling operations that can succeed or fail Practical, not theoretical..

When deciding which approach to use, consider the specific requirements of your application. If the types share a common base or interface, inheritance and polymorphism are usually the best choice. Think about it: if you need maximum flexibility and are willing to sacrifice some type safety, object or dynamic may be appropriate. For more type-safe and expressive code, discriminated unions or generics are often the best solutions Practical, not theoretical..

In practice, the choice of method depends on factors such as the need for type safety, the design of your class hierarchy, and the clarity of your code. By understanding the strengths and limitations of each approach, you can choose the one that best fits your needs and write cleaner, more maintainable C# code.

To wrap this up, C# provides several ways to return one of two types, each with its own trade-offs. Whether you use inheritance, object, dynamic, tuples, generics, or discriminated unions, the key is to choose the method that best balances flexibility, type safety, and clarity for your specific scenario. By doing so, you can write reliable and maintainable code that is easy to understand and extend That alone is useful..

Honestly, this part trips people up more than it should.

Leveraging Pattern Matching for Concise Dispatch

When the two possible return types are known ahead of time, C# 9 introduced switch expressions that can de‑construct a discriminated union in a single line. Consider the following illustration:

Result HandleRequest(Request req)
{
    return req switch
    {
        SuccessPayload s => Result.Success(s.Value),
        FailurePayload f => Result.Failure(f.Code),
        _ => throw new InvalidOperationException("Unexpected payload")
    };
}

The compiler validates each pattern against the static type of the object, guaranteeing that only valid cases are reachable. This eliminates the need for runtime is checks and makes the flow of control explicit at a glance. On top of that, pattern matching works easily with record types, allowing you to define immutable payloads that carry additional metadata without inflating the public API.

Balancing Performance and Readability

  • Inheritance‑based hierarchies incur a virtual dispatch, which can be costly in tight loops or high‑frequency APIs. If performance is a primary concern, a lightweight struct‑based discriminated union can be preferable.
  • object/dynamic incurs boxing and runtime type checks, which may degrade throughput and also sidesteps compile‑time verification. Use them sparingly, typically when interoperability with legacy code or dynamic languages is required.
  • Tuples and generics are essentially syntactic sugar; they add no runtime overhead beyond the generic constraints themselves. That said, they can become unwieldy when the tuple contains more than a couple of elements, leading to cryptic deconstruction code.

A pragmatic rule of thumb is to prototype the simplest solution first—often a tuple or a discriminated union—and only switch to a more heavyweight approach if profiling reveals a bottleneck.

Advanced Scenarios: Nested Results and Async Pipelines

Real‑world code rarely stops at a single binary outcome. Frequently you need to compose multiple steps, each of which may succeed or fail. The monadic bind pattern, popularized by functional languages, maps naturally onto C# through extension methods:

public static async Task> BindAsync(
    this Task> source,
    Func>> selector)
{
    var result = await source.ConfigureAwait(false);
    return result.Success ? selector(result.Value) : Task.FromResult(result);
}

By chaining BindAsync calls, you can build pipelines where each stage propagates failure without boilerplate if/else nesting. This is especially valuable when dealing with asynchronous I/O, database calls, or HTTP requests that may each return a distinct error type The details matter here..

Choosing the Right Tool: A Decision Matrix

Requirement Recommended Technique
Strict compile‑time type safety Discriminated union, generic Result<T>
Need for runtime polymorphism Interface/abstract class hierarchy
Minimal ceremony, quick prototyping Tuple ((TSuccess, TError)) or object wrapper
High‑throughput, low‑latency path Struct‑based discriminated union with readonly
Interfacing with dynamic or COM APIs dynamic (sparingly)
Composable asynchronous workflows Monad‑style BindAsync / Result extensions

Final Thoughts

C# offers a rich palette of mechanisms for returning one of two distinct types, each tuned to a different set of constraints. Worth adding: by evaluating the problem through the lenses of safety, performance, and maintainability, you can select the construct that aligns with both the immediate need and the long‑term architecture of your codebase. Whether you opt for a lean tuple, a reliable discriminated union, or a full‑featured polymorphic hierarchy, the goal remains the same: express intent clearly, avoid ambiguity, and keep the path to future evolution as smooth as possible That alone is useful..

In short, the optimal choice is the one that lets you write code that is both correct today and adaptable tomorrow.

Handling Complex Dependencies

As pipelines grow, managing dependencies between stages becomes crucial. Practically speaking, consider scenarios where a later stage requires the output of an earlier one, but that earlier stage itself depends on another. A common approach is to introduce a dependency injection container or a similar mechanism to manage these relationships. That's why decouple stages and make them more testable and reusable becomes possible here. Here's the thing — alternatively, you can make use of techniques like factory methods or builder patterns to construct complex objects with their dependencies in a controlled manner. Beyond that, consider using techniques like event-driven architectures to loosely couple stages, allowing them to react to changes in other parts of the pipeline without direct dependencies.

Error Handling Strategies Beyond Simple Failure

While Result<T, E> elegantly handles success and failure, more sophisticated error handling strategies may be required. Techniques like circuit breakers, retry mechanisms, and bulkheads can significantly improve the resilience of your asynchronous pipelines. A circuit breaker, for instance, will temporarily halt requests to a failing service after a certain number of errors, preventing cascading failures. Retries, intelligently implemented with exponential backoff, can recover from transient issues. Bulkheads isolate concurrent operations, preventing one failing operation from overwhelming the entire pipeline. Integrating these patterns into your Result-based approach requires careful consideration, often involving wrapping individual stages with error handling logic and propagating errors appropriately through the pipeline.

Performance Considerations: Minimizing Overhead

Although Result<T, E> provides a powerful abstraction, it’s important to be mindful of potential performance overhead. For high-throughput scenarios, consider using struct-based discriminated unions with readonly fields to minimize memory allocation and improve performance. Adding to this, avoid unnecessary computations within the selector function of BindAsync. Profiling your code is essential to identify any bottlenecks. In practice, the boxing and unboxing of value types within discriminated unions, particularly when used extensively, can introduce a slight performance penalty. Optimize the operations performed at each stage to confirm that the overall pipeline remains efficient.

Conclusion

The journey of mastering error handling and asynchronous workflows in C# is one of thoughtful selection and strategic implementation. On the flip side, the tools – discriminated unions, Result types, tuples, and even dynamic – each possess unique strengths and weaknesses. Now, ultimately, the most effective approach isn’t about rigidly adhering to a single pattern, but rather about understanding the specific requirements of your application and choosing the construct that best balances safety, performance, and maintainability. By embracing a pragmatic, problem-oriented mindset and continuously profiling and refining your code, you can build dependable, resilient, and adaptable asynchronous pipelines that deliver reliable results, even in the face of complexity and uncertainty. Remember that the goal is not simply to return a Result, but to architect a system that gracefully handles errors, manages dependencies, and performs efficiently – a testament to well-crafted and considered C# code The details matter here. Turns out it matters..

The official docs gloss over this. That's a mistake.

Just Made It Online

Fresh Out

Others Liked

Picked Just for You

Thank you for reading about C# Return One Of Two Types. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home