Search code examples
c#const-correctnesscompile-time-constant

What is the best approach or alternative to constant references?


For the purposes of this question, a 'constant reference' is a reference to an object from which you cannot call methods that modify the object or modify it's properties.

I want something like this:

Const<User> user = provider.GetUser(); // Gets a constant reference to an "User" object
var name = user.GetName(); // Ok. Doesn't modify the object
user.SetName("New value"); // <- Error. Shouldn't be able to modify the object

Ideally, I would mark with a custom attribute (e.g. [Constant]) every method of a class that doesn't modify the instance, and only those methods can be called from the constant reference. Calls to other methods would result in an error, if possible, during compile time.

The idea is I can return a read-only reference to and be sure that it will not be modified by the client.


Solution

  • The technique you're referring to is called "const-correctness" which is a language feature of C++ and Swift, but not C#, unfortunately - however you're onto something by using a custom attribute because that way you can enforce it via a Roslyn extension - but that's a rabbit-hole.

    Alternatively, there's a much simpler solution using interfaces: because C# (and I think the CLR too) does not support const-correctness (the closest we have is the readonly field modifier) the .NET base-class-library designers added "read-only interfaces" to common mutable types to allow a object (wheather mutable or immutable) to expose its functionality via an interface that only exposes immutable operations. Some examples include IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlyDictionary<T> - while these are all enumerable types the technique is good for singular objects too.

    This design has the advantage of working in any language that supports interfaces but not const-correctness.

    1. For each type (class, struct, etc) in your project that needs to expose data without risk of being changed - or any immutable operations then create an immutable interface.
    2. Modify your consuming code to use these interfaces instead of the concrete type.

    Like so:

    Supposing we have a mutable class User and a consuming service:

    public class User
    {
        public String UserName     { get; set; }
    
        public Byte[] PasswordHash { get; set; }
        public Byte[] PasswordSalt { get; set; }
    
        public Boolean ValidatePassword(String inputPassword)
        {
            Hash[] inputHash = Crypto.GetHash( inputPassword, this.PasswordSalt );
            return Crypto.CompareHashes( this.PasswordHash, inputHash );
        }
    
        public void ResetSalt()
        {
            this.PasswordSalt = Crypto.GetRandomBytes( 16 );
        }
    }
    
    public static void DoReadOnlyStuffWithUser( User user )
    {
        ...
    }
    
    public static void WriteStuffToUser( User user )
    {
        ...
    }
    

    Then make an immutable interface:

    public interface IReadOnlyUser
    {
        // Note that the interfaces' properties lack setters.
        String              UserName     { get; }
        IReadOnlyList<Byte> PasswordHash { get; }
        IReadOnlyList<Byte> PasswordSalt { get; }
    
        // ValidatePassword does not mutate state so it's exposed
        Boolean ValidatePassword(String inputPassword);
    
        // But ResetSalt is not exposed because it mutates instance state
    }
    

    Then modify your User class and consumers:

    public class User : IReadOnlyUser
    {
        // (same as before, except need to expose IReadOnlyList<Byte> versions of array properties:
        IReadOnlyList<Byte> IReadOnlyUser.PasswordHash => this.PasswordHash;
        IReadOnlyList<Byte> IReadOnlyUser.PasswordSalt => this.PasswordSalt;
    }
    
    public static void DoReadOnlyStuffWithUser( IReadOnlyUser user )
    {
        ...
    }
    
    // This method still uses `User` instead of `IReadOnlyUser` because it mutates the instance.
    public static void WriteStuffToUser( User user )
    {
        ...
    }