C# List Declaration: Syntax and Initialization
c# list declaration: Learn the correct syntax for declaring and initializing List<T> in C#, including collection initializers, capacity, and common mistakes.
c# list declaration requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
Declaring a List<T> in C# is straightforward, but the exact syntax you choose affects readability, performance, and maintainability. The most common form is List<T> myList = new List<T>();, which creates an empty list with the default capacity. You can also use a collection initializer to populate the list at declaration time: List<int> numbers = new List<int> { 1, 2, 3 };. This article covers the declaration syntax, initialization patterns, type inference with var, and the tradeoffs involved in each approach.
Basic Declaration and Initialization
The simplest declaration creates an empty list with no elements:
List<string> names = new List<string>();
This is equivalent to var names = new List<string>(); when the type is obvious from the right-hand side. The var keyword is a common choice because it reduces redundancy, but it can reduce clarity when the type is not immediately apparent from the initializer.
To initialize a list with known values, use a collection initializer:
List<int> scores = new List<int> { 90, 85, 88 };
This compiles to repeated calls to Add, so it is functionally identical to creating an empty list and adding each element manually. The initializer is purely syntactic sugar, but it makes the intent clear and reduces the chance of forgetting to add an element.
You can also set the initial capacity when you know the approximate number of elements in advance:
List<Customer> customers = new List<Customer>(100);
This pre-allocates internal storage for 100 references, avoiding multiple resizes during growth. It does not limit the list to 100 items; the list will still grow beyond that capacity if needed.
Using var for Type Inference
var is often used in list declarations to keep the code concise:
var items = new List<Order>();
The compiler infers the type as List<Order>. This is safe and does not weaken type safety. However, consider readability: if the initializer is complex or the type is not obvious, an explicit type may be clearer. For example:
var lookup = new Dictionary<string, List<int>>();
The type is still clear from the right-hand side, but some developers prefer the explicit form for complex generic types.
A common mistake is to use var with a collection initializer that creates an array instead of a list:
var numbers = { 1, 2, 3 }; // Invalid
This is not valid C#. You must specify the collection type, either as new List<int> { ... } or new[] { 1, 2, 3 } for an array.
Common Declaration Mistakes
One frequent error is declaring a list without initializing it, then attempting to add elements:
List<int> numbers; numbers.Add(1); // NullReferenceException
A list variable must be assigned an instance before use. The compiler does not force initialization for fields, but local variables must be definitely assigned before they are read. The safest pattern is to initialize at declaration or in a constructor.
Another mistake is confusing arrays with lists. Arrays have fixed size and are declared with int[] or new int[] { ... }. Lists are resizable and use List<T>. If you need to add or remove elements dynamically, a list is the appropriate choice.
A less obvious mistake is using a list when a read-only collection would be more appropriate. If the collection is never modified after construction, consider IReadOnlyList<T> or ReadOnlyCollection<T> to signal immutability. This is not a declaration error, but it improves API design.
Choosing Between List and Other Collections
List<T> is the default choice for a resizable collection that supports index access. However, other collection types may be better in specific scenarios:
| Collection | Strengths | Weaknesses | Best Use Case |
|---|---|---|---|
List<T> | Index access, fast enumeration, dynamic size | Insertions/removals in the middle are O(n) | General-purpose resizable collection |
LinkedList<T> | O(1) insertions/removals at ends | No index access, higher memory overhead | Frequent insert/delete at ends |
HashSet<T> | O(1) lookups, uniqueness | Unordered, no index access | Set operations, deduplication |
Dictionary<TKey,TValue> | O(1) lookups by key | Requires unique keys, no order | Key-value mapping |
Use List<T> when you need ordered, indexable access and do not require frequent middle insertions. For a fixed-size collection, an array is more efficient in memory and access speed, but you must know the size at compile time.
Performance Considerations for List Declarations
The internal storage of a List<T> is an array. When you add elements beyond the current capacity, the list allocates a new, larger array and copies the existing elements. This is an O(n) operation. By setting the initial capacity to a value close to the expected final size, you can avoid multiple resizes and reduce memory churn.
For example, if you are reading a file with a known number of lines, you can declare:
var lines = new List<string>(lineCount);
This reduces the number of allocations. However, setting capacity too high wastes memory if the list ends up much smaller. The default capacity is 4, and it grows by doubling when exceeded.
Another performance consideration is the use of List<T> versus arrays in hot paths. Arrays have slightly better cache locality and avoid the overhead of the List<T> wrapper, but the difference is usually negligible unless you are doing millions of operations. Always profile before optimizing.
Thread Safety and Concurrency
List<T> is not thread-safe. If multiple threads read and write the same list concurrently, you must synchronize access. A common pattern is to use a lock:
private readonly object _lock = new object(); private List<int> _items = new List<int>(); public void AddItem(int item) { lock (_lock) { _items.Add(item); } }
For read-heavy scenarios, consider using ConcurrentBag<T> or ConcurrentQueue<T> from System.Collections.Concurrent. These are designed for concurrent access but have different performance characteristics. If you need a snapshot of the list for iteration, copy it under a lock to avoid exceptions during enumeration.
Advanced Declaration Patterns
You can combine list declaration with LINQ to create a list from a query:
var evenNumbers = Enumerable.Range(1, 100).Where(n => n % 2 == 0).ToList();
This is a concise way to build a list from a sequence. The ToList() extension method materializes the query into a List<int>.
Another pattern is declaring a list of a custom type with an object initializer:
var employees = new List<Employee> { new Employee { Id = 1, Name = "Alice" }, new Employee { Id = 2, Name = "Bob" } };
This is common when building test data or in-memory collections.
When you need a list that can be modified but only exposes read-only access to callers, declare the variable as List<T> internally but return IReadOnlyList<T> from a property:
private List<Order> _orders = new List<Order>(); public IReadOnlyList<Order> Orders => _orders;
This prevents callers from modifying the list directly while still allowing internal mutation.
Compatibility and Language Versions
Collection initializers have been available since C# 3.0, so they work in all modern versions. The var keyword is also from C# 3.0. The List<T> class has been part of .NET since the beginning. No special using directive is required because List<T> is in the System.Collections.Generic namespace, which is imported by default in most project templates. If you are working in a script or a minimal context, you may need to add using System.Collections.Generic; explicitly.
For older .NET Framework versions, the behavior is the same. The main difference is that newer .NET versions have improved performance for some operations, but the declaration syntax is unchanged.