Back to Blog
C#

C# Member Variables Explained with Examples

c# member variables: 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.

c# member variables requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you declare a variable directly inside a class or struct, it becomes a c# member variable — commonly called a field. These variables 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, but they run before any constructor logic. This ordering matters when a field initializer depends on another field that isn't initialized yet.

public class Invoice { private decimal _subtotal = 100m; private decimal _tax = _subtotal * 0.08m; // Compiles, but _subtotal is already set }

Field initializers execute in the order they appear in the class declaration. This is consistent for all constructed instances. If a field initializer throws an exception, the object construction fails before the constructor body runs. For complex initialization that depends on constructor parameters or external resources, assign values inside the constructor instead.

Constructor assignment is 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. This means the static initializer runs only once per application domain.

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 primitive type (such as int, double, bool, char, or string) or an enum. 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, 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, use immutable fields that are assigned only in a constructor, which eliminates the risk of race conditions entirely.

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

Instance fields are stored in managed heap for reference types. Every object allocation includes its instance fields. The fields of a class are part of the object's memory footprint. 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 typically happens when the application domain or process ends). 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

Field initializers run before the constructor body, in textual declaration order. This becomes problematic when one field initializer references another instance field that hasn't been initialized yet.

public class Pipeline { private int _step = 2; private int _next = _step + 1; // This is fine because _step is already assigned. }

But the following is a compile-time error:

public class BrokenPipeline { private int _next = _step + 1; // _step not yet assigned private int _step = 2; }

The compiler rejects this because _step is used before its declaration. When field initializers depend on each other, rely on constructor assignment to control the order explicitly.

Another ordering issue occurs when fields are used by a method called from the constructor. If a virtual method is called from the constructor, derived class field initializers have already run, but derived constructor bodies have not. This can lead to using default values when a derived field's initializer hasn't executed yet. Avoid calling virtual methods from constructors to prevent such subtle bugs.

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 modesty (private) should be obvious from the name alone.

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 only for primitives; 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: Practical Usage and Code Examples | RYUSLOG DEV