How to use readonly vs const vs static in C#

In C#, readonly, const, and static are keywords used to define variables with different behaviors in terms of mutability, memory allocation, and scope.

Understanding their differences is crucial for writing efficient and maintainable code. In this article we'll take a look at each and see how they are used.

1. const (Constant Values)

A const variable is a compile-time constant, meaning its value must be assigned at declaration and cannot be changed later.

Key Characteristics:

  • Must be assigned at declaration.
  • Stored in the assembly metadata (not allocated memory at runtime).
  • Can only be assigned primitive types, string, or enum values.
  • Cannot be modified after compilation.

Example:

public class MathConstants
{
    public const double Pi = 3.14159;
}

// Usage:
Console.WriteLine(MathConstants.Pi); // Output: 3.14159

⚠ Limitations:

  • Since const values are replaced at compile-time, updating a const in a library requires recompiling all dependent projects.
  • Cannot use non-primitive types (e.g., objects, lists).

2. readonly (Runtime Immutable Fields)

A readonly field allows initialization either at declaration or in the constructor but cannot be modified afterward.

Key Characteristics:

  • Can be assigned at declaration or inside a constructor.
  • Its value can change during runtime (but only in the constructor).
  • Works with all data types, including objects.
  • More flexible than const since values are resolved at runtime.

Example:

public class Circle
{
    public readonly double Radius;
    public readonly double Pi = 3.14159;

    public Circle(double radius)
    {
        Radius = radius; // Allowed because it's inside the constructor.
    }
}

// Usage:
Circle c = new Circle(5);
Console.WriteLine(c.Radius); // Output: 5

✔ Best for: Values that should remain constant per instance but need to be assigned dynamically at runtime.


3. static (Shared Across All Instances)

A static variable belongs to the type itself rather than to any instance of the class.

Key Characteristics:

  • Shared across all instances of a class.
  • Cannot be used with instance constructors.
  • Initialized once and persists for the application’s lifetime.
  • Can be combined with readonly or const.

Example:

public class GlobalConfig
{
    public static string ApplicationName = "MyApp";
    public static readonly DateTime StartTime = DateTime.Now;
}

// Usage:
Console.WriteLine(GlobalConfig.ApplicationName); // Output: MyApp

✔ Best for: Global state, caching, configuration values, and utility methods.


Key Differences Summary

Feature const readonly static
Mutability Immutable Immutable (after construction) Mutable
When Set Compile-time Runtime (constructor) Runtime
Memory Usage Stored in metadata Instance-based Type-based (shared)
Can Use Objects? ❌ No ✅ Yes ✅ Yes
Can Change After Initialization? ❌ No ❌ No (after constructor) ✅ Yes

Choosing the Right One:

  • Use const for fixed, compile-time values that will never change.
  • Use readonly for immutable values that need runtime initialization.
  • Use static for class-level data shared across all instances.

Understanding these differences helps you write cleaner, more efficient C# code. Happy coding! 🚀

3
106

Related

Working with dates is a common requirement in many applications, and calculating the difference between two dates is a particularly frequent task.

C# provides several powerful built-in methods to handle date arithmetic efficiently. Let's explore how to calculate date differences in C#.

Using DateTime and TimeSpan

The most straightforward way to calculate the difference between two dates in C# is by using the DateTime struct and the TimeSpan class:

DateTime startDate = new DateTime(2023, 1, 1);
DateTime endDate = new DateTime(2023, 12, 31);

TimeSpan difference = endDate - startDate;

Console.WriteLine($"Total days: {difference.TotalDays}");
Console.WriteLine($"Total hours: {difference.TotalHours}");
Console.WriteLine($"Total minutes: {difference.TotalMinutes}");
Console.WriteLine($"Total seconds: {difference.TotalSeconds}");

Getting Specific Units

Sometimes you need the difference in specific units (years, months, days). The TimeSpan class doesn't directly provide years and months, since these units vary in length. Here's how to handle this:

int years = endDate.Year - startDate.Year;
int months = endDate.Month - startDate.Month;

if (months < 0)
{
    years--;
    months += 12;
}

// Adjust for day differences
if (endDate.Day < startDate.Day)
{
    months--;
    int daysInMonth = DateTime.DaysInMonth(startDate.Year, startDate.Month);
    int dayDifference = daysInMonth - startDate.Day + endDate.Day;
    Console.WriteLine($"Years: {years}, Months: {months}, Days: {dayDifference}");
}
else
{
    int dayDifference = endDate.Day - startDate.Day;
    Console.WriteLine($"Years: {years}, Months: {months}, Days: {dayDifference}");
}

Using DateTimeOffset for Time Zone Awareness

If your application needs to handle dates across different time zones, consider using DateTimeOffset:

DateTimeOffset startDateOffset = new DateTimeOffset(2023, 1, 1, 0, 0, 0, TimeSpan.FromHours(-5));
DateTimeOffset endDateOffset = new DateTimeOffset(2023, 12, 31, 0, 0, 0, TimeSpan.FromHours(1));

TimeSpan timeDifference = endDateOffset - startDateOffset;
Console.WriteLine($"Total days including time zone difference: {timeDifference.TotalDays}");

Practical Applications

Date difference calculations are useful in many scenarios:

  • Calculating age from birth date
  • Determining duration between events
  • Computing business days between dates
  • Scheduling recurring events

With these techniques, you can handle most date arithmetic requirements in your C# applications efficiently and accurately.

6
859

Primary constructors, introduced in C# 12, offer a more concise way to define class parameters and initialize fields.

This feature reduces boilerplate code and makes classes more readable.

Traditional Approach vs Primary Constructor

Before primary constructors, you would likely write something like the following:

public class UserService
{
    private readonly ILogger _logger;
    private readonly IUserRepository _repository;

    public UserService(ILogger logger, IUserRepository repository)
    {
        _logger = logger;
        _repository = repository;
    }

    public async Task<User> GetUserById(int id)
    {
        _logger.LogInformation("Fetching user {Id}", id);
        return await _repository.GetByIdAsync(id);
    }
}

With primary constructors, this becomes:

public class UserService(ILogger logger, IUserRepository repository)
{
    public async Task<User> GetUserById(int id)
    {
        logger.LogInformation("Fetching user {Id}", id);
        return await repository.GetByIdAsync(id);
    }
}

Key Benefits

  1. Reduced Boilerplate: No need to declare private fields and write constructor assignments
  2. Parameters Available Throughout: Constructor parameters are accessible in all instance methods
  3. Immutability by Default: Parameters are effectively readonly without explicit declaration

Real-World Example

Here's a practical example using primary constructors with dependency injection:

public class OrderProcessor(
    IOrderRepository orderRepo,
    IPaymentService paymentService,
    ILogger<OrderProcessor> logger)
{
    public async Task<OrderResult> ProcessOrder(Order order)
    {
        try
        {
            logger.LogInformation("Processing order {OrderId}", order.Id);
            
            var paymentResult = await paymentService.ProcessPayment(order.Payment);
            if (!paymentResult.Success)
            {
                return new OrderResult(false, "Payment failed");
            }

            await orderRepo.SaveOrder(order);
            return new OrderResult(true, "Order processed successfully");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
            throw;
        }
    }
}

Tips and Best Practices

  1. Use primary constructors when the class primarily needs dependencies for its methods
  2. Combine with records for immutable data types:
public record Customer(string Name, string Email)
{
    public string FormattedEmail => $"{Name} <{Email}>";
}
  1. Consider traditional constructors for complex initialization logic

Primary constructors provide a cleaner, more maintainable way to write C# classes, especially when working with dependency injection and simple data objects.

3
76

Reading a file line by line is useful when handling large files without loading everything into memory at once.

✅ Best Practice: Use File.ReadLines() which is more memory efficient.

Example

foreach (string line in File.ReadLines("file.txt"))
{
    Console.WriteLine(line);
}

Why use ReadLines()?

Reads one line at a time, reducing overall memory usage. Ideal for large files (e.g., logs, CSVs).

Alternative: Use StreamReader (More Control)

For scenarios where you need custom processing while reading the contents of the file:

using (StreamReader reader = new StreamReader("file.txt"))
{
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(line);
    }
}

Why use StreamReader?

Lets you handle exceptions, encoding, and buffering. Supports custom processing (e.g., search for a keyword while reading).

When to Use ReadAllLines()? If you need all lines at once, use:

string[] lines = File.ReadAllLines("file.txt");

Caution: Loads the entire file into memory—avoid for large files!

6
349