An array in C# is built around one core assumption: every element is the same type. Storing heterogeneous data C# developers actually work with, a mix of integers, strings, custom objects, whatever a real-world dataset actually contains, means working against that assumption, not with it, and the four or five ways to do it each trade off type safety, performance, and code clarity differently. This guide of C# array best practices covers what actually works for storing heterogeneous data in C# arrays, the real performance and safety costs of each approach, and where C# still has a genuine gap compared to languages built around this problem more directly.
The Core Problem: Why Arrays Resist Heterogeneity
A C# array’s type (int[], string[], Customer[]) is fixed at declaration, and the CLR enforces it strictly, you can’t store a string in an int[] no matter how you try. This isn’t a limitation to work around by accident; it’s the specific guarantee that lets arrays be fast, since the runtime knows exactly how much memory each element needs without runtime type checking on every access. Storing a genuine C# mixed type array means deliberately opting out of that guarantee somewhere, and every approach below is really a different answer to “where do I want to pay that performance overhead.” C# generics heterogeneous storage is possible too, though generics alone (a List<T> with T fixed to one type) don’t actually solve true heterogeneity, they solve type safety for a single type, which is a different, narrower problem.
Option 1: The C# Object Array
object[] mixedData = new object[4];
mixedData[0] = 42;
mixedData[1] = “hello”;
mixedData[2] = 3.14;
mixedData[3] = new Customer(“Jane”);
Every type in C# ultimately derives from System.Object, so an object[] can technically hold anything. This is the most flexible option and the one most beginner tutorials reach for first, but it comes with real, measurable costs. Value types (int, double, struct) get boxed, wrapped in a heap-allocated object, every time they’re stored, and unboxed every time they’re read back, which is genuinely slower than working with a strongly-typed array and creates extra garbage collection pressure at scale. It also pushes all type-checking to runtime: retrieving mixedData[0] gives you back an object, and you need an explicit cast or pattern match before you can actually use it as an int, with no compile-time safety net if you get the type wrong.
Best for: Small-scale, genuinely mixed data where performance isn’t the priority and the code reading the array can reliably handle runtime type checks.
Option 2: List<object> or List<T> with a Common Base
List<object> items = new List<object> { 42, “hello”, 3.14 };
Functionally similar to an object array, same boxing costs, same runtime-only type safety, but with the resizing convenience List<T> provides that a fixed-size array doesn’t. This is really the C# List vs array question in practice: a List<object> gives you dynamic growth at the same type-safety cost as object[]. If your “heterogeneous” data actually shares a common base class or interface, using List<BaseType> instead of List<object> recovers meaningful type safety: you know every element is at least a BaseType, even if the specific derived type varies.
Best for: Collections that need to grow dynamically, or where the mixed types genuinely share a meaningful common ancestor worth typing against directly.
Option 3: The C# Dynamic Array with dynamic
dynamic[] items = new dynamic[3];
items[0] = 42;
items[1] = “hello”;
items[2] = new Customer(“Jane”);
items[0].SomeMethod(); // resolved at runtime, not compile time
dynamic defers all type resolution to runtime, which means code compiles even if the method or property you’re calling doesn’t exist on the actual object, you find out with an exception, not a compile error. This is genuinely useful for interop scenarios (COM objects, reflection-heavy code) but is close to the worst option for general heterogeneous storage: it’s slower than object in most benchmarks due to the runtime binding overhead, and it silently gives up the compiler’s ability to catch mistakes early, which is usually exactly the safety net you want when working with mixed data.
Best for: Interop and reflection-heavy scenarios specifically, rarely the right default choice for general-purpose heterogeneous data storage.
Option 4: Tuples and Records for Structured Heterogeneity
var record = (Id: 42, Name: “hello”, Price: 3.14); // a ValueTuple under the hood
public record Product(int Id, string Name, decimal Price);
The tuple syntax above compiles down to a ValueTuple, C#’s built-in structural type for exactly this bounded case. Note this is a genuinely different tool than C# generics, generics parameterize a single type across a collection, while tuples and records bundle several different types together as one fixed-shape value.
When the “heterogeneous” data isn’t arbitrary, it’s actually a fixed, known structure with different types per field, like an ID, a name, and a price, tuples and C# records (introduced in C# 9), documented officially here, are a genuinely better fit than a general-purpose object array. A record gives you named fields, value-based equality, and full compile-time type safety, at the cost of needing to define the shape upfront rather than storing arbitrary types ad hoc.
Best for: Data that’s heterogeneous in type but consistent in structure, most real-world “mixed data” scenarios are actually this, not truly arbitrary type mixing.
What C# Still Doesn’t Have: Discriminated Unions
Worth stating honestly rather than glossing over: C# does not have native discriminated unions as of current language versions, a real, known gap compared to F#, which is built around exactly this pattern (a value that’s definitely one of a fixed set of types, with the compiler enforcing exhaustive handling). Developers coming from F#, TypeScript, or Rust often look for this specifically when storing heterogeneous data and are surprised it isn’t a built-in C# feature. The closest current approximations are a class hierarchy combined with pattern matching (switch expressions checking type patterns), or a third-party library, but neither is quite the same compiler-enforced exhaustiveness a true discriminated union provides.
Performance and Safety Comparison
| Approach | Type Safety | Performance | Best Use Case |
| object[] | Runtime only | Boxing overhead | Small, genuinely arbitrary mixed data |
| List<object> | Runtime only | Boxing overhead + resize cost | Dynamic collections, arbitrary types |
| List<BaseType> | Compile-time (partial) | No boxing for reference types | Mixed types sharing a common ancestor |
| dynamic | None (runtime binding) | Slowest, runtime method resolution | Interop and reflection scenarios |
| Records/Tuples | Full compile-time | Best, no boxing, no runtime checks | Fixed, known heterogeneous structure |
Best Practices Summary
- Default to a common base type or interface: Over object whenever the mixed types share any meaningful behavior, you recover real type safety at essentially no cost.
- Use records for structured heterogeneous data: most real scenarios described as “mixed types” are actually fixed-shape data that a record represents better than a generic array.
- Reserve dynamic for genuine interop scenarios: COM interop, reflection-heavy code, not as a general-purpose shortcut around type safety.
- Use pattern matching, not repeated casting: when reading from an object[] or List<object> — switch expressions with type patterns are clearer and catch more mistakes than a chain of is/as checks.
- Measure before optimizing for boxing overhead: it’s a real cost, but for small collections or infrequent access, it’s rarely the actual bottleneck in a real application; don’t over-engineer around a cost that isn’t measurably affecting your specific workload. This is the same discipline that matters for any performance-sensitive code review, profile first, optimize what the data actually shows, not what seems theoretically slow.
The Bottom Line
Storing heterogeneous data in C# arrays comes down to being honest about what “heterogeneous” actually means in your specific case. Genuinely arbitrary mixed types are rare in practice, most real scenarios are either types sharing a common ancestor (use that base type directly) or a fixed structure with different field types (use a record). Reach for object[] or dynamic only when the data really is arbitrary, and know upfront that both trade real performance and compile-time safety for that flexibility, a trade worth making deliberately, not by default.
Building This Into a Real Application
Getting these data-modeling decisions right early is exactly the kind of architecture work that’s cheap to do correctly upfront and expensive to unwind later, especially at scale. This is core application development work, the same category of decision that shows up constantly in real custom software projects, not something unique to arrays specifically. If you’re building a real application and want a second opinion on your data structures before they’re load-bearing, reach out if you want it reviewed properly.
Frequently Asked Questions
How to store different data types in a C# array?Â
Use object[] for genuinely arbitrary types, a common base type array if the types share an ancestor, or a record if the data has a fixed, known structure, each trades type safety and performance differently, covered in detail above.
C# object array vs List performance: which is actually faster?Â
Both incur the same boxing overhead for value types. List<T> adds resizing convenience object[] doesn’t have, at a small additional cost for dynamic growth, for a fixed-size collection, a plain array is marginally faster; for a growing collection, List<T> is usually the better trade.
What’s the best way to store mixed types in C#?Â
It depends on whether the types are genuinely arbitrary or share structure. Most real “mixed type” scenarios are actually fixed-shape data better served by a record than a general-purpose object array, reserve object[] for cases that are truly arbitrary.
Does C# have discriminated unions?Â
Not natively, as of current language versions, a real gap compared to F#. The closest approximations are class hierarchies combined with pattern matching, or third-party libraries, neither providing true compiler-enforced exhaustiveness.
When to use dynamic vs object in C#?Â
Use object by default, with an explicit cast or pattern match when reading values back, it’s faster and gives you compile-time safety on the cast itself. Reserve dynamic specifically for interop or reflection-heavy scenarios where runtime method resolution is genuinely necessary, not as a general substitute for object.
When should I use records instead of an object array for mixed data?Â
When the data has a known, fixed structure, different types per field, but the same fields every time. Records give full compile-time type safety and better semantics than a general-purpose array for this common case.
Is List<object> slower than a strongly-typed List<T>?Â
Yes, for value types specifically, boxing and unboxing overhead applies the same way it does with object[]. For reference types, the difference is smaller but you still lose compile-time type safety.
What’s a C# array of mixed data types example that actually performs well?Â
A record or readonly struct with explicitly typed fields, (int Id, string Name, decimal Price), outperforms object[] for the same data because there’s no boxing and no runtime type checks, provided the structure is known ahead of time rather than genuinely arbitrary.