C#
generics
List
type-safety
object-oriented programming

In C, why can't a Liststring object be stored in a Listobject variable

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, every string is an object, but List<string> is not a List<object>. That is not a strange compiler limitation. It is a deliberate type-safety rule. If the assignment were allowed, you could insert non-string objects into a list that was supposed to contain only strings.

Why the Assignment Fails

This does not compile:

csharp
List<string> names = new List<string> { "Alice", "Bob" };
List<object> objects = names;

At first glance it looks reasonable. After all, string derives from object. The problem appears when mutation enters the picture.

If the assignment were legal, then this would also become legal:

csharp
objects.Add(42);

But objects and names would refer to the same underlying list. That would mean an integer had been added to a List<string>, which breaks the type guarantee of the original collection.

List<T> Is Invariant

List<T> is invariant. That means List<Derived> is not convertible to List<Base>, and List<Base> is not convertible to List<Derived>, even when Derived inherits from Base.

This is necessary because List<T> is both readable and writable:

  • you can read items out as T
  • you can add items in as T

Once a generic container allows writes, the type system has to prevent unsafe substitutions.

Covariance Works Only for Safe Read-Only Shapes

C# does support covariance for certain interfaces, such as IEnumerable<out T>, because they only expose values for reading.

csharp
1IEnumerable<string> names = new List<string> { "Alice", "Bob" };
2IEnumerable<object> objects = names;
3
4foreach (object item in objects)
5{
6    Console.WriteLine(item);
7}

This is safe because IEnumerable<T> does not let you insert arbitrary object values back into the sequence.

That is the key distinction: covariance is allowed when the API shape makes it safe.

If You Really Need List<object>, Copy the Items

If the goal is to create a separate list typed as object, project into a new list:

csharp
1using System.Linq;
2
3List<string> names = new() { "Alice", "Bob" };
4List<object> objects = names.Cast<object>().ToList();
5
6objects.Add(42);
7
8Console.WriteLine(names.Count);   // 2
9Console.WriteLine(objects.Count); // 3

Now the two collections are different objects, so adding an integer to objects does not corrupt names.

Design APIs Around the Real Requirement

A lot of confusion comes from method signatures that ask for List<object> even though they only need to read values. In those cases, a read-only abstraction is usually better.

csharp
1static void PrintAll(IEnumerable<object> values)
2{
3    foreach (var value in values)
4    {
5        Console.WriteLine(value);
6    }
7}
8
9PrintAll(new List<string> { "Alice", "Bob" });

This design works because the method consumes a covariant interface rather than a mutable list.

Arrays Are a Historical Exception

Developers sometimes point out that string[] can be assigned to object[]. That is true in .NET, but arrays are covariant for historical reasons and can fail at runtime in exactly the kind of unsafe situation that generics try to prevent.

So this is legal but dangerous:

csharp
object[] values = new string[] { "Alice" };
values[0] = 42; // runtime exception

Generics were designed more strictly so the compiler can reject unsafe cases earlier.

Common Pitfalls

The biggest mistake is assuming subtype relationships automatically lift through generic containers. They do not unless the generic type is explicitly variant and used in a safe way.

Another issue is requiring List<object> where an IEnumerable<object> or another read-only interface would be enough. That forces callers into unnecessary conversions.

People also sometimes treat array covariance as evidence that the list conversion should work too. In reality, arrays are the older, weaker design and one reason generics were made stricter.

Finally, do not confuse string is object with List<string> is List<object>. Those are completely different type relationships.

Summary

  • 'List<string> cannot be assigned to List<object> because List<T> is invariant.'
  • Allowing the assignment would make it possible to insert non-string values into a string list.
  • Covariance works for read-only interfaces such as IEnumerable<T>.
  • If you need a List<object>, create a new copied list.
  • Prefer read-only abstractions in APIs when mutation is not required.

Course illustration
Course illustration

All Rights Reserved.