Back to Blog
C#

C# Member Variables Explained with Examples

Understand C# member variables: field syntax, initialization, static vs instance, readonly and const, and when properties are the better choice.

C# FieldsInstance MembersStatic MembersReadonly and ConstProperty Initialization
Diagram of C# class with labeled member variables showing instance, static, and readonly fields.

Member variables in C# are the variables declared directly inside a class or struct. They are commonly called fields and store state for each object or for the type itself. A field is the simplest form of state in C#, but choosing between field, property, static, readonly, or const affects memory behavior, API design, and thread safety.

The core syntax for a field is straightforward:

public class Order { private decimal _subtotal; private decimal _taxRate = 0.08m; protected int _lineCount; internal string _customerId; }

Each field has an access modifier, a type, a name, and optionally an initializer. The initializer runs when the object is constructed, before the constructor body executes. Fields declared without an initializer receive the default value for their type — zero for numeric types, null for reference types, and false for bool.

Choosing Field Access Modifiers

The access modifier on a field controls its visibility across assemblies and inheritance. Private fields are the default recommendation because they preserve encapsulation — only the containing type can read or modify them. Protected fields allow derived classes to access them directly, but this creates a tight coupling between base and derived implementations. If a derived class depends on a protected field's value, changing how the base class manages that state can silently break the derived behavior.

Public fields are rarely appropriate. Exposing a field directly means callers can set any value without validation, and changing the field to a property later breaks binary compatibility. Use a property with a private backing field when you need external code to read or write state, even if no validation or computed logic exists yet. This keeps the public API stable and lets you add logic later without changing callers.

Initializing Member Variables in C#

Fields can be initialized inline or in constructors. Inline initializers are convenient for simple default values. They run before the constructor body, and multiple inline initializers run in the order they appear. However, an instance field initializer cannot reference another instance field, so this example does not compile:

public class Invoice { private decimal _subtotal = 100m; private decimal _tax = _subtotal * 0.08m; // Does not compile }

If a field's starting value depends on another field or on a constructor parameter, assign it inside the constructor instead. Constructor assignment is also the standard place for dependencies passed in at runtime:

public class DiscountCalculator { private readonly decimal _rate; private readonly decimal _minimumOrderValue; public DiscountCalculator(decimal rate, decimal minimumOrderValue) { _rate = rate; _minimumOrderValue = minimumOrderValue; } public decimal Calculate(decimal orderTotal) { if (orderTotal < _minimumOrderValue) return 0m; return orderTotal * _rate; } }

Using a readonly field with constructor assignment prevents accidental changes after construction. The compiler enforces that readonly fields are assigned only in the declaration or in a constructor, which makes the object's state immutable after creation. This is particularly useful for configuration values or dependency references that should not change during the object's lifetime.

Distinguishing Static and Instance Member Variables

A field belongs to an instance by default: each object carries its own copy. In contrast, a static field belongs to the type itself and is shared across all instances. This distinction is fundamental to how memory is allocated and how data is accessed.

public class Counter { private static int _totalCreated; private int _instanceNumber; public Counter() { _instanceNumber = ++_totalCreated; } public int InstanceNumber => _instanceNumber; public static int TotalCreated => _totalCreated; }

Here, _totalCreated is shared by all Counter instances. Every time a new instance is created, the static field increments, and the instance field stores that value. Static fields are initialized when the type is first used, not when each instance is created. The static initializer runs once for that type.

Static fields are useful for caching values that are expensive to compute per-instance and that remain constant across instances. However, static mutable state introduces concurrency concerns. Multiple threads can read and write the same static field simultaneously, so updates must be synchronized or the values must be immutable after initialization.

Instance fields are generally safer for data that varies per object. They align with the object's identity and avoid accidental sharing of state between logically distinct objects.

Comparing readonly and const Fields

C# offers two ways to create fields whose value never changes after initialization: const and readonly. They behave differently at compile time and runtime.

const fields are compile-time constants. Their values are embedded directly in the consuming code at compile time. The value must be a constant expression of a built-in type (numeric types including decimal, bool, char, string) or an enum type. You cannot use const with new, DateTime, or any reference type other than string.

public class MathConstants { public const double Pi = 3.14159; public const string Label = "Math"; }

Because const values are inlined by the compiler, changing a public const in a library requires recompiling all consumers. If you do not recompile, they still use the old value.

readonly fields are runtime constants. They can be assigned in the field declaration or in a constructor — an instance constructor for instance fields, a static constructor for static fields — and the value is evaluated at runtime. This allows them to hold DateTime, arrays, or other reference types.

public class AppSettings { public static readonly DateTime StartedAt = DateTime.UtcNow; public static readonly string[] AllowedHosts = { "api.example.com", "admin.example.com" }; }

Readonly fields behave like normal fields in terms of sharing — they are copied per instance unless declared static. Use const when you need compile-time substitution for a primitive that will never change across versions. Use static readonly for values that require runtime initialization and are too complex for const, or when you need to avoid the recompilation issue.

Member Variables and Thread Safety

Since instance fields can be modified through methods, their thread safety depends on how they are accessed. Reading a field is atomic for reference types and for value types that fit within the platform's native word size. Multi-word value types like decimal or long on 32-bit systems are not guaranteed to be read atomically. In those cases, concurrent reads and writes can produce torn values — a read that sees a mix of old and new bits.

The practical approach is to keep mutable fields private and use synchronization primitives like lock or Interlocked when sharing them across threads. Alternatively, design fields so they are assigned only in a constructor and never mutated afterward; this removes race conditions caused by direct field writes.

public class Account { private readonly object _balanceLock = new object(); private decimal _balance; public decimal Balance { get { lock (_balanceLock) { return _balance; } } } public void Credit(decimal amount) { lock (_balanceLock) { _balance += amount; } } }

This example protects access to the _balance field with a lock. The lock ensures that reads and writes are atomic with respect to each other, preventing lost updates. Without synchronization, concurrent calls to Credit could overwrite each other's increments.

Consider the tradeoff between locking overhead and correctness. For simple counters, Interlocked.Increment avoids a lock but only works on primitive numeric types. For complex state transitions, a lock is easier to reason about and prevents subtle concurrency bugs.

Properties vs Fields in the Public Surface

Fields and properties serve different roles. A field is a storage location. A property is a member that exposes a getter and/or setter, often backed by a private field. Properties can include validation, lazy initialization, or computed logic without changing how the rest of the code interacts with the object.

public class Temperature { private double _celsius; public double Celsius { get => _celsius; set => _celsius = value; } public double Fahrenheit { get => _celsius * 9 / 5 + 32; } }

Here, Celsius exposes a settable field through a property, while Fahrenheit computes its value on the fly. This abstraction keeps the internal representation unchanged while providing a convenient external API. If you later switch to storing Fahrenheit internally, only the property implementations change; consumers remain unaffected.

Auto-properties eliminate the explicit backing field:

public class Customer { public string Name { get; set; } public string Email { get; set; } }

The compiler generates a private backing field automatically. Auto-properties are the proper default for public and protected state because they give you the freedom to add logic later.

In contrast, a public field forces all consumers to access the storage directly. Changing it to a property later breaks source and binary compatibility. For new code, prefer properties over public fields.

Memory and Lifetime Considerations for Member Variables

For class instances, instance fields are stored on the managed heap as part of the object. Every object allocation includes its instance fields. For struct instances, fields are stored inline wherever the struct value is stored. The actual memory used depends on the types of the fields: a byte field occupies one byte among potentially aligned allocation chunks, while a string field stores a reference that points to a separate heap object.

Static fields live in the type's static storage area. They exist even before any instance is created and persist for the application's lifetime. A large static collection that is no longer needed remains in memory until the type is unloaded, which in most applications is when the process exits. This can lead to memory bloat if you use static fields for data that should have a shorter lifecycle.

Consider this example:

public static class GlobalCache { public static ConcurrentDictionary<string, byte[]> Data = new(); }

This static field holds a reference to a dictionary that may grow unboundedly. If entries are never removed, memory usage increases steadily. Using instance fields for per-object data keeps memory aligned with object lifecycles; using static fields requires explicit cleanup or an eviction policy.

When Member Variable Initialization Order Breaks

Multiple inline field initializers run in the order they appear, before the constructor body. That order matters if an initializer throws, but it does not allow one instance field initializer to reference another instance field. If one field's starting value depends on another, assign it in the constructor:

public class Pipeline { private int _step = 2; private int _next; public Pipeline() { _next = _step + 1; } }

Ordering can also be a problem with constructors and virtual method calls. When a base constructor calls a virtual method, the derived override runs after the derived class's field initializers but before the derived constructor body. Any field that the override expects to be set in the derived constructor body is still at its default value. Avoid calling virtual methods from constructors to prevent this class of bug.

Backing Field Conventions and Naming

Common .NET conventions use camelCase with a leading underscore for private fields, for example _balance. This distinguishes fields from local variables and property names. Some codebases prefer no underscore and use camelCase only. Whatever convention you choose, apply it consistently.

For private static fields, a common pattern is s_ prefix (e.g., s_instanceLock) to indicate static scope, though the underscore prefix is more widespread. If you adopt the underscore convention, static fields are simply _totalCount. Clarity is the goal: the field name should reveal its purpose, and its private scope should be obvious from the name itself.

Common Pitfalls with Member Variables in C#

One frequent mistake is using public fields when properties are needed. This exposes raw data and makes future changes harder. Another is making fields virtual — fields cannot be virtual in C#. Trying to override a field in a derived class only hides the base field with a new one, leading to confusing behavior. Use properties for polymorphic behavior.

A third pitfall is overusing static fields for shared state without considering thread safety. A static collection with multiple writers will corrupt or throw exceptions. If you need global state, ensure it is thread-safe and has a bounded lifetime.

Finally, confusing const and readonly causes compilation or runtime surprises. Remember const is compile-time inlined and limited to compile-time constants; readonly is runtime and can reference more complex objects.

Understanding the distinctions among field kinds, their initialization rules, and their concurrency properties lets you choose the right member variable for each design. These decisions affect memory usage, API stability, and the correctness of multithreaded code.

C# Member Variables: Fields, Static vs Instance, Readonly vs Const, and Properties | RYUSLOG DEV