Menu

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! 🚀

0
66

Related

Storing passwords as plain text is dangerous. Instead, you should hash them using a strong, slow hashing algorithm like BCrypt, which includes built-in salting and resistance to brute-force attacks.

Step 1: Install BCrypt NuGet Package

Before using BCrypt, install the BCrypt.Net-Next package:

dotnet add package BCrypt.Net-Next

or via NuGet Package Manager:

Install-Package BCrypt.Net-Next

Step 2: Hash a Password

Use BCrypt.HashPassword() to securely hash a password before storing it:

using BCrypt.Net;

string password = "mySecurePassword123";
string hashedPassword = BCrypt.HashPassword(password);

Console.WriteLine(hashedPassword); // Output: $2a$12$...

Step 3: Verify a Password

To check a user's login attempt, use BCrypt.Verify():

bool isMatch = BCrypt.Verify("mySecurePassword123", hashedPassword);
Console.WriteLine(isMatch); // Output: True

Ensuring proper hashing should be at the top of your list when it comes to building authentication systems.

2
217

XML (Extensible Markup Language) is a widely used format for storing and transporting data.

In C#, you can create XML files efficiently using the XmlWriter and XDocument classes. This guide covers both methods with practical examples.

Writing XML Using XmlWriter

XmlWriter provides a fast and memory-efficient way to generate XML files by writing elements sequentially.

Example:

using System;
using System.Xml;

class Program
{
    static void Main()
    {
        using (XmlWriter writer = XmlWriter.Create("person.xml"))
        {
            writer.WriteStartDocument();
            writer.WriteStartElement("Person");

            writer.WriteElementString("FirstName", "John");
            writer.WriteElementString("LastName", "Doe");
            writer.WriteElementString("Age", "30");

            writer.WriteEndElement();
            writer.WriteEndDocument();
        }
        Console.WriteLine("XML file created successfully.");
    }
}

Output (person.xml):

<?xml version="1.0" encoding="utf-8"?>
<Person>
    <FirstName>John</FirstName>
    <LastName>Doe</LastName>
    <Age>30</Age>
</Person>

Writing XML Using XDocument

The XDocument class from LINQ to XML provides a more readable and flexible way to create XML files.

Example:

using System;
using System.Xml.Linq;

class Program
{
    static void Main()
    {
        XDocument doc = new XDocument(
            new XElement("Person",
                new XElement("FirstName", "John"),
                new XElement("LastName", "Doe"),
                new XElement("Age", "30")
            )
        );
        doc.Save("person.xml");
        Console.WriteLine("XML file created successfully.");
    }
}

This approach is ideal for working with complex XML structures and integrating LINQ queries.

When to Use Each Method

  • Use XmlWriter when performance is critical and you need to write XML sequentially.
  • Use XDocument when you need a more readable, maintainable, and flexible way to manipulate XML.

Conclusion

Writing XML files in C# is straightforward with XmlWriter and XDocument. Choose the method that best suits your needs for performance, readability, and maintainability.

1
64

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.

0
66