Understanding C# Class Members
c# class members: Learn how C# class members define data and behavior: fields, properties, methods, constructors, events, and static members with practical examples.
When you design a class in C#, the members you declare determine what the class can store, how it behaves, and how other code interacts with it. The set of c# class members includes fields, properties, methods, constructors, events, and nested types. Each member type serves a distinct purpose, and choosing the right one for a given responsibility keeps the class maintainable and predictable.
Fields: The Data Storage of a Class
A field is the simplest member: a variable declared directly in the class body. It holds the state of an instance or, if declared static, the state of the type itself. Fields have no built-in logic; they are read and written directly.
public class Order { public decimal TotalAmount; private DateTime _createdAt; }
Here TotalAmount is a public field, and _createdAt is a private field. Public fields are rarely a good idea because they expose the internal representation without any control. Private fields are the typical storage behind properties.
Fields can be initialized inline, which runs before the constructor body:
public class Order { private DateTime _createdAt = DateTime.UtcNow; }
Use fields when you need a simple storage location that is only accessible within the class. For any data that must be validated or computed when accessed, a property is a better fit.
Properties: Controlled Access to Data
Properties expose data with get and set accessors, allowing you to add logic without changing the public API. They are the standard way to expose state in C#.
public class Customer { private string _name; public string Name { get => _name; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Name cannot be empty."); _name = value; } } }
The property wraps the private field _name and validates input before assigning it. You can also use auto-implemented properties when no extra logic is needed:
public class Customer { public string Name { get; set; } }
Auto-properties create a hidden backing field automatically. They are useful for simple data transfer objects where validation or computed behavior is unnecessary.
Properties can be read-only (no setter), write-only (no getter, rare), or have different accessibility for getter and setter. For example, a property that is publicly readable but only settable within the class:
public class Order { public DateTime CreatedAt { get; private set; } }
This pattern is common for immutable values that must be set during construction.
Methods: Defining Behavior
Methods encapsulate operations that a class can perform. They can accept parameters, return values, and modify the internal state.
public class Calculator { public int Add(int a, int b) { return a + b; } public void Reset() { // Clear any state } }
Methods can be overloaded to provide different parameter combinations, and they can be virtual or abstract to support polymorphism. The choice between a method and a property is often about whether the operation involves computation or side effects. A property should be cheap and not throw exceptions, while a method can perform more complex work.
Constructors: Initializing Class Instances
Constructors are special methods that run when an instance is created. They ensure the object starts in a valid state. You can define multiple constructors with different parameter lists.
public class Product { public string Name { get; } public decimal Price { get; } public Product(string name, decimal price) { Name = name; Price = price; } public Product(string name) : this(name, 0) { } }
The second constructor chains to the first using : this(...), reusing the validation logic. If you don't declare any constructor, the compiler provides a default parameterless one. Once you add a constructor, that default is no longer generated.
Static constructors run once per type, before any instance is created or static member is accessed. They are useful for initializing static state.
public class DatabaseSettings { public static string ConnectionString { get; private set; } static DatabaseSettings() { ConnectionString = LoadFromConfig(); } }
Static constructors are called automatically and cannot be invoked directly.
Events: Notifying External Code
Events allow a class to broadcast that something happened, without knowing which other objects are listening. They are based on delegates and follow a publisher-subscriber pattern.
public class Button { public event EventHandler? Clicked; public void SimulateClick() { Clicked?.Invoke(this, EventArgs.Empty); } }
Subscribers attach handlers using += and detach with -=. The ?.Invoke pattern ensures the event is only raised if there are subscribers. Events are commonly used in UI frameworks and for signaling state changes.
Nested Types: Organizing Related Classes
A class can contain other types as members, such as nested classes, structs, or enums. This is useful when the nested type is only relevant in the context of the outer class.
public class Order { public enum Status { Pending, Shipped, Delivered } public Status CurrentStatus { get; set; } }
The nested Status enum is accessed as Order.Status. Nested types can be private to hide implementation details, or public to expose a related type.
Static Members: Sharing State Across Instances
Static members belong to the type itself, not to any instance. They are shared across all instances and accessed using the type name.
public class Counter { public static int InstanceCount; public Counter() { InstanceCount++; } }
Static fields and properties are useful for configuration, shared caches, or counters. Static methods are often used for utility functions that don't depend on instance state.
Static members introduce a concurrency concern: if multiple threads access a static field, you need synchronization to avoid race conditions. For example, incrementing InstanceCount without locking is not thread-safe. In modern C#, you might use Interlocked.Increment or a lock. Static constructors are thread-safe by default, but static field access is not.
Choosing the Right Member for the Job
Selecting the appropriate member type depends on what you are trying to model:
| Member | Use when | Example |
|---|---|---|
| Field | Internal storage with no access control | _privateCounter |
| Property | Exposing data with validation or computed behavior | public string Name { get; set; } |
| Method | Performing an operation or computation | CalculateTotal() |
| Constructor | Initializing a new instance | public Order(int id) |
| Event | Notifying subscribers of a state change | public event EventHandler? Changed |
| Nested type | A type that only makes sense inside the containing class | Order.Status |
| Static | State or behavior shared across all instances | public static int Count |
A common mistake is using a public field when a property is needed. Fields cannot be changed later to add validation without breaking the API. Properties give you that flexibility. Similarly, using a method for a simple value that could be a property makes the code more verbose and can confuse callers who expect property syntax.
When a class grows, keep members focused. If a method does too many things, consider splitting it into smaller methods or moving logic to another class. The same applies to properties: if a getter performs expensive work, it should probably be a method named Calculate or Fetch, not a property.
Static members should be used sparingly. They make testing harder because they introduce global state. If you need shared configuration, consider dependency injection instead of a static property. For thread safety, always synchronize access to mutable static fields.
By understanding the role of each member type, you can design classes that are clear, maintainable, and safe to use in production. The choice between a field and a property, or an instance and a static method, directly affects how the class can evolve and how it behaves under concurrency.