Back to Blog
C#

C# Return Multiple Values: Tuples, Out Parameters, and Custom Types

c# return multiple values: Learn how to return multiple values from a C# method using tuples, out parameters, and custom types, with practical examples and tradeoffs.

C#TuplesOut ParametersValueTupleMethod Design
Illustration of a C# method returning multiple values using a tuple, showing the concept of multiple return values.

c# return multiple values requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

In C#, a method signature declares a single return type. When you need to return multiple values, you must choose a way to package them. The options range from lightweight tuples to dedicated types, and each choice affects how the caller consumes the result and how the runtime handles allocation. This article compares the common approaches for returning multiple values and explains the tradeoffs.

The Single Return Type Constraint

A C# method can return only one value directly. This is a fundamental language rule. If you need to return two or more related pieces of data, you must combine them into a single object. The language gives you several ways to do that, each with different implications for readability, performance, and maintainability.

The most common approaches are:

  • Using a tuple, either System.Tuple or System.ValueTuple
  • Using out parameters
  • Defining a custom type, such as a class or record

Each approach solves the same core problem, but the right choice depends on how often the method is called, how many values are involved, and whether the result represents a meaningful domain concept.

Using System.Tuple for Grouped Values

The System.Tuple class has been part of the .NET Framework since version 4.0. It is a reference type, so a tuple instance is allocated on the heap. You create one with Tuple.Create or the Tuple<...> constructor, and you access its members through properties like Item1, Item2, and so on.

public Tuple<int, string> GetUserInfo() { return Tuple.Create(42, "Alice"); } var info = GetUserInfo(); Console.WriteLine($"ID: {info.Item1}, Name: {info.Item2}");

The properties Item1 and Item2 are not descriptive. This hurts readability when the tuple is used far from the method definition. The caller must remember what each position represents. Also, because Tuple is a class, every call allocates a new object. In hot paths, this can add pressure on the garbage collector.

System.Tuple is still valid code, but it is rarely the best choice for new development. The .NET team introduced ValueTuple to address both the naming problem and the allocation overhead.

ValueTuple and the Tuple Literal Syntax

ValueTuple is a struct, so it is typically allocated on the stack or inline within another object. It supports named members, which makes the result self-documenting. The C# 7.0 tuple literal syntax lets you write (int id, string name) directly as a return type.

public (int Id, string Name) GetUserInfo() { return (42, "Alice"); } var user = GetUserInfo(); Console.WriteLine($"ID: {user.Id}, Name: {user.Name}");

The names Id and Name are part of the method signature. The compiler preserves them in the metadata, so IntelliSense and other tools can show them. This is a major improvement over Item1 and Item2.

You can also deconstruct the result directly into separate variables:

(int id, string name) = GetUserInfo(); Console.WriteLine($"ID: {id}, Name: {name}");

Because ValueTuple is a struct, it avoids heap allocation in most cases. The runtime can keep the values on the stack or inline them in a containing object. This makes it a good fit for methods that are called frequently and return a small number of values.

One limitation is that ValueTuple is a general-purpose container. It does not express domain meaning. A method returning (int, string) could represent a user ID and name, or an order number and status. The names help, but they are not enforced by the type system. If the meaning is central to your design, a custom type may be clearer.

Out Parameters for Optional and Multiple Returns

The out modifier lets a method assign values to variables passed by the caller. You can declare multiple out parameters, which gives you a way to return several values without creating a container type.

public bool TryGetUserInfo(int id, out string name, out int age) { if (id <= 0) { name = null; age = 0; return false; } name = "Alice"; age = 30; return true; } if (TryGetUserInfo(42, out string name, out int age)) { Console.WriteLine($"Name: {name}, Age: {age}"); }

The out parameters must be assigned before the method returns. This is enforced by the compiler. The caller must pass variables that will receive the values, and those variables do not need to be initialized beforehand.

out parameters are most useful when the primary return value is a boolean indicating success or failure, and the other values are the result of a successful operation. This pattern is common in parsing and dictionary lookup methods, such as int.TryParse or Dictionary.TryGetValue.

However, out parameters have drawbacks. They cannot be used with async methods, because the compiler does not allow out parameters in methods that return Task. They also make the method signature harder to read when there are many parameters. A method with three or four out parameters becomes unwieldy and error-prone at the call site.

Custom Types for Complex Return Values

When the returned values represent a cohesive concept, defining a dedicated type is often the clearest approach. A class or record can carry named properties, validation logic, and behavior. This is especially useful when the same shape is returned from multiple methods or used in many places.

public record UserInfo(int Id, string Name, int Age); public UserInfo GetUserInfo() { return new UserInfo(42, "Alice", 30); } var user = GetUserInfo(); Console.WriteLine($"ID: {user.Id}, Name: {user.Name}, Age: {user.Age}");

A record provides value equality and a concise declaration. A class gives you more control over mutable state and inheritance. The choice depends on whether the object should be immutable and how it will be used.

Custom types add a small amount of boilerplate, but they make the contract explicit. The type name communicates the meaning. You can add methods that operate on the data, and you can evolve the type without changing every call site. If the result is used across many layers, a custom type is usually worth the extra definition.

Performance and Allocation Considerations

The main performance difference among these approaches is allocation behavior.

  • System.Tuple is a class. Every instance is a heap allocation. Frequent calls can increase garbage collection pressure.
  • ValueTuple is a struct. It is allocated inline or on the stack, so it avoids heap allocation in most scenarios. The exception is when you box it, for example by casting to object or storing it in a non-generic collection.
  • out parameters do not allocate a container at all. The values are written directly into variables the caller provides. This is the most allocation-friendly option.
  • Custom types are classes or records. If you define a record as a class, each instance is a heap allocation. If you define a struct record, it behaves like ValueTuple.

For most application code, the allocation difference is not the deciding factor. The clarity and maintainability of the code usually matter more. But in a tight loop that returns a tuple millions of times, using ValueTuple or out parameters can reduce measurable overhead.

Another consideration is the size of the returned data. A ValueTuple with many fields becomes a large struct, which can hurt performance when it is copied frequently. If you need to return more than a handful of values, a reference type may actually be more efficient because passing the reference is cheaper than copying a large struct.

Choosing the Right Approach

The decision depends on how the result is used and what the method represents.

Use ValueTuple when:

  • The values are loosely related and only needed together for a single call.
  • You want to avoid defining a new type for a one-off result.
  • The method is called frequently, and you want to minimize allocations.

Use out parameters when:

  • The primary return value is a boolean indicating success or failure.
  • The other values are only meaningful when the method succeeds.
  • You want to avoid allocating any container, even a struct.

Use a custom type when:

  • The values represent a domain concept that appears in multiple places.
  • You need to add behavior or validation to the returned data.
  • The result is part of a public API and should be self-documenting.

There is no universal best option. The right approach balances readability, performance, and the structure of your codebase. A method that returns two related values in a small internal helper is well served by a ValueTuple. A method that returns a customer record across service boundaries is better off with a dedicated type.

One more pattern worth knowing is the ref return, which lets you return a reference to a field or array element. It is useful in specific scenarios like implementing a Span<T>-based collection, but it is not a general-purpose way to return multiple values. For most code, the techniques described here cover the need.

c# return multiple values: Practical Usage and Code Examples | RYUSLOG DEV