Understanding the C# new Keyword: Operator, Modifier, and Constraint
Learn the three roles of the C# new keyword: the new operator, the new modifier, and the new() constraint, with code examples and common pitfalls.
The C# new keyword appears in three distinct roles in the language: the new operator for creating instances, the new modifier for hiding inherited members, and the new() constraint for generic type parameters. Each role has its own syntax, behavior, and design implications. Understanding when each applies prevents compiler warnings and subtle runtime surprises.
The new Operator for Object Creation
The most common use of the new keyword is the new operator, which creates an instance and invokes a constructor. For reference types, it allocates an object on the managed heap. For value types, it initializes an instance; the storage location depends on the context. The operator works with classes, structs, arrays, and delegates.
var list = new List<int>(); var point = new Point(3, 4); var numbers = new int[] { 1, 2, 3 }; Action greet = new Action(() => Console.WriteLine("Hello"));
The new operator can also be used with object and collection initializers, which let you set properties or add elements in the same expression:
var person = new Person { Name = "Ada", Age = 36 }; var dictionary = new Dictionary<string, int> { { "key", 1 } };
The dictionary line uses a collection initializer, which calls Add for each entry. The object initializer sets properties after the constructor runs.
When you use new on a struct, the constructor runs and all fields are assigned before the value is used. For a class, the constructor may throw, leaving the reference unassigned. You must use new to call a constructor in ordinary C# code; default(T) creates a default value without running a constructor.
The new Modifier for Member Hiding
The new modifier explicitly hides a member inherited from a base class. This is different from overriding. When you hide a member, the derived class defines a new member with the same name and signature, and calls through the base type are not routed to it. The compiler reports CS0108 if you hide a member without new; adding new suppresses that warning and documents intent.
class Base { public void Display() => Console.WriteLine("Base"); } class Derived : Base { public new void Display() => Console.WriteLine("Derived"); }
Calling Display through a Base reference invokes Base.Display; through a Derived reference it invokes Derived.Display. This is a compile-time decision rather than virtual dispatch. Use new when you deliberately want to replace a member for callers who use the derived type directly, but you do not want to change behavior for callers holding a base reference.
new also works on properties, events, fields, and nested types. The base member keeps its own access level; the new member has the access you declare. If you need polymorphic behavior, use override with a virtual base member instead.
The new() Constraint for Generic Type Parameters
The new() constraint requires a generic type argument to have a public parameterless constructor. This allows you to create instances of the type parameter inside a generic method or class without knowing the concrete type at compile time.
public T CreateInstance<T>() where T : new() { return new T(); }
The constraint can be combined with other constraints, such as class or an interface or base class. For example, where T : class, new() requires a reference type with a public parameterless constructor. There is no need to combine new() with struct, because every value type already has an implicit parameterless constructor. The constraint is enforced at compile time; a type that does not have a public parameterless constructor will not satisfy it.
new() works only with parameterless constructors. If you need to pass constructor arguments, use a factory delegate or reflection. An interface or abstract class cannot satisfy a new() constraint by itself, because the type must be constructible when used as a type argument. A concrete class that implements an interface can satisfy a combined constraint such as where T : IThing, new().
Choosing the Right Use of the new Keyword
Because the keyword appears in three contexts, it is easy to confuse them. The table below summarizes the purpose and behavior of each.
| Use | Purpose | Runtime Behavior |
|---|---|---|
new operator | Create an instance | Allocates or initializes an instance and runs constructor |
new modifier | Hide an inherited member | Compile-time binding to derived member |
new() constraint | Allow generic instantiation | Requires parameterless constructor at compile time; runtime support handles construction |
When you see new in a declaration, it is either a modifier or a constraint. When you see new in an expression, it is the operator. The context determines the meaning, and the compiler enforces the correct usage.
Common Mistakes and Compiler Warnings
A frequent mistake is hiding a member unintentionally. If you define a method in a derived class with the same signature as a base method, and the base method is not virtual, the compiler warns CS0108. Adding the new modifier clarifies that the hiding is intentional. Ignoring the warning leaves the code ambiguous and may lead to calls resolving differently than expected.
Another mistake is expecting where T : new() to be satisfied by a type that has only a parameterized constructor. This results in a compile-time error, not a runtime failure. For example, a class with only a parameterized constructor cannot satisfy where T : new(). If you need to support such types, consider using a factory function or Activator.CreateInstance with appropriate checks.
For the new operator, a common error is assuming that new on a value type always allocates on the managed heap. In many local-variable cases, value types are stored on the stack or inline, but boxing can place them on the heap. The new operator itself initializes the value; the storage location depends on context.
Performance and Allocation Considerations
For reference types, the new operator allocates an object on the managed heap and runs a constructor. For value types, it initializes a value without necessarily allocating heap memory. Frequent allocation of short-lived reference objects can increase garbage-collection pressure. If you are creating many small objects in a loop, consider reusing instances or using structs when appropriate. However, do not optimize prematurely; measure with a profiler before changing design.
The new modifier has no runtime cost by itself because it is a compile-time directive; it changes which member a non-virtual call resolves to. The new() constraint is also a compile-time requirement. In a generic method, new T() is generic code, not a call to a specific constructor known at compile time; the runtime handles constructor dispatch for the actual T. Prefer new() when you only need a parameterless constructor, because it expresses the requirement directly in source and lets the compiler verify it.
Maintainability and Design Implications
Using the new modifier can be a design smell. If you find yourself hiding base members frequently, reconsider whether inheritance is the right abstraction. Hiding can lead to confusing behavior when code mixes base and derived references. Prefer virtual methods and override when you want polymorphic behavior. Reserve new hiding for cases where you are extending a third-party type and cannot modify the base class.
The new() constraint is a clean way to express that a generic type must be constructible without arguments. It makes the generic contract explicit and enables the compiler to verify it. When combined with interfaces, it allows factories to create instances without coupling to a specific implementation.
For the new operator, prefer object initializers for readability when setting multiple properties. This reduces the number of statements and keeps the object construction local. However, object initializers are not atomic; if a property setter throws, the object is partially constructed. For critical initialization, use a constructor that validates all inputs.