The C# type system distinguishes between value types and reference types with real consequences for performance and behavior. A struct is a value type typically allocated on the stack, copied on assignment, and zero-initialized by default; a class is a reference type allocated on the heap, assigned by reference, and null by default. When a struct is assigned to an object or interface variable, it is boxed, copying the value to the heap, and the reverse requires an explicit cast. Records (C# 9+) and record structs (C# 10+) provide value-based equality and a with expression for non-destructive mutation, blurring the line between class and struct idioms. Structs cannot inherit from other structs, while classes support single inheritance, and large mutable structs suffer from copy semantics that hurt performance. Two struct instances with the same field values are value-equal, while two class instances with the same fields are reference-equal by default unless Equals and GetHashCode are overridden or the type is a record.
Generics let you parameterize classes, methods, and interfaces over a type, providing type safety at compile time without the cost of boxing. Constraints expressed with where clauses, such as class, struct, new(), a base class, or an implemented interface, narrow the allowed type arguments. Interfaces and delegates also support variance: covariance (out T) lets a generic type be used as a more derived type, while contravariance (in T) lets it be used as a less derived type. Types often implement IEquatable
C# provides rich syntax for declaring members. Properties, including auto-implemented and expression-bodied forms, encapsulate fields behind get and set accessors, and init-only setters enable immutable initialization. Indexers allow array-like access with any parameter type. Extension methods add methods to existing types without modifying them, provided they live in a static class and use the this modifier on the first parameter, with instance methods taking precedence over matching extensions. Access modifiers (public, private, protected, internal, protected internal, private protected, and file in C# 11) control visibility, with internal types scoped to the same assembly. An assembly is the compiled output of a project, a DLL or EXE that contains IL, metadata, and a manifest. The partial keyword lets a type be split across multiple files, which is common in code generation. Constants are inlined at compile time, while readonly fields are runtime constants settable in a constructor; a static class can hold only static members and is implicitly sealed and abstract, while a sealed class can be instantiated but cannot be inherited, and a static constructor runs once per type before the first instance is created or any static member is accessed.
Polymorphism in C# takes several forms. Subtype polymorphism, enabled by virtual and override, lets the runtime type determine which method executes; abstract methods have no implementation and must be overridden, while virtual methods provide a default. Operator overloading lets you redefine operators like +, -, and == on custom types, with paired overloads and matching Equals and GetHashCode expected. Interfaces define contracts that implementing classes must provide, and C# 8+ allows default interface methods so an interface can ship a default implementation; explicit interface implementation makes a member accessible only through an interface reference, useful for resolving naming conflicts between multiple interfaces. The var keyword is statically typed with the type inferred at compile time, while dynamic bypasses compile-time checks via the Dynamic Language Runtime. The is operator tests type compatibility and can extract a value via pattern matching, while as performs a safe cast that returns null on failure; is null and == null differ when == is overloaded. The Convert class uses banker's rounding on doubles and tolerates null, while an explicit cast on a double truncates toward zero. Method parameters support ref (must be initialized, may be read and written), out (need not be initialized, must be assigned by the method), and in (read-only reference that avoids copies of large structs); ref locals and ref returns let you bind directly to a variable for in-place modification of struct fields. MemberwiseClone makes a shallow copy, while deep copies require recursive duplication. The checked and unchecked keywords control whether integer overflow throws OverflowException or silently wraps.