Search code examples
c#structconstantsfinalreadonly

Is there a good way to make runtime constants within methods in C#?


I really enjoy how in Java, we can declare variables "final" within methods to indicate that they will not change after initialization. In C#, you cannot do this same thing-- the only options are const (only works for primitives) and readonly (only works for fields declared outside of methods), neither of which I want.

I've discovered I can do something akin to the following to make a pseudo-final struct:

public readonly struct Final<T> {
    public readonly T Value;

    public Final(T val) {
        Value = val;
    }


    public static implicit operator T(Final<T> final) => final.Value;
    public static implicit operator Final<T>(T val) => new Final<T>(val);
}

Here's my issue: I want to be able to use this Final struct and treat it as if all I have is the datatype it's storing. For example, assume I have a class which stores 2D vectors called Vector2, with an x and y parameter. I would want to be able to write something like this:

Final<Vector2> rightVec = new Vector2(3.0f, 0.0f);
int yComponent = rightVec.y

However, I can't do the above. Instead, I'd have to write something like this:

Final<Vector2> rightVec = new Vector2(3.0f,0.0f);
int yComponent = rightVec.Value.y // <-- I don't want to have to write "Value"!

So this brings me to my questions. First, is there a way to do what I want? Second, is there a better method to do what I want than with structs? Third, is it even a good idea to try to do this?


Solution

  • Don't consider final locals as a feature. Opposed to read-only fields or constants, that are introduced to optimize compiled code, final locals were introduced to overcome a short coming of the Java engine (design mistakes).
    Java later had issues implementing lambdas (or closures in general). In particular the case when lambdas (aka anonymous classes) capture local variables.

    Although we have to differentiate between read-only final (Java) or readonly (C#) and constant static final (Java) or const (C#), it's important to note that C# does support local constants:

    C#

    public void SomeMethod()
    {
      const int number = 1234;
    
      // Compiler error
      number = 0;
    }
    

    Locals defined as final (read-only) don't offer any relevant performance improvement except for really rare edge cases (like string concatenation, where there already exist more efficient alternative solutions like string builders). For fields it does as the compiler can statically evaluate values at compiletime and choose to inline them.
    In fact, while compiletime constants do enable performance optimizations Java doesn't support them. However, C# supports local compiletime constants via the const keyword.

    In addition, having read-only fields (class or in stance variables) allows a type to be defined as immutable. For locals this is irrelevant - especially in the pass-by-value context of Java.
    C#, in contrast to Java, allows explicit pass-by-reference.
    In this context a final local e.g. a read-only method parameter would absolutely make sense to prevent the reference from being overwritten by the consumer. C# has the in keyword for this case.

    C#

    public void SomeMethod(in int number)
    {
      // Compiler error
      number = 12;
    }
    

    As we can see, using the in keyword allows us to achieve a similar effect: read-only local variables. For this reason, I would consider this an equivalent to Java's final local (from the usage point of view in terms of read-only locals and not in terms of the original purpose of final locals or compiler details).

    I don't think there is any justification for read-only locals in C# from a compiler perspective. You may find it beneficial from a static analyzer perspective (for this purpose the keyword would have to be used sparingly). However, there are better ways in my opinion that allow for read-only variables and provide compiler and static analyzers support, in both languages.

    The way you are thinking of a final local variable (which is not what the keyword final was designed for) is to define a read-only value that is guaranteed to not change. From that point of view (and not from the closure point of view!) declaring a constant field is the exact same. Therefore, I would consider C# const an equivalent to final local variables in Java (only in terms of read-only values!):

    Java

    class MyClass {
    
      // Constants: more powerful than final locals in many aspects
      // e.g., compiler optimizations like inlining 
      // or readability improvements like grouping constant values 
      // in one place to know them at a glance and change them in one place etc.
      private static final int NUMBER_OF_REPEATS = 8;
    
      public void DoSomething() {
    
        // This variable is "constant" but redefined on every method call.
        // Why not make it a static field and allow the compiler 
        // to perform statical optimizations like inlining?
        final int numberOfRepeats = 8;
      
        // Instead of...
        int repeatCount = 0;
        while (repeatCount++ < numberOfRepeats) {
        }
      
        // ...you may use a constant like below. 
        // Compare the readability: it's instantly obvious that 
        // NUMBER_OF_REPEATS is a constant value, by the naming convention alone 
        // - without having to read the variable definition to identify the 'final' keyword.
        int repeatCount = 0;
        while (repeatCount++ < MyClass.NUMBER_OF_REPEATS) {
    
        }
      }
    }
    

    C#
    The C# equivalent (in terms of a read-only variable!).

    class MyClass
    {
      // More powerful than final locals in many aspects
      // e.g., compiler optimizations like inlining 
      // or readability improvements like grouping constant values 
      // in one place to know them at a glance and change them in one place etc.
      private const int NumberOfRepeats = 8;
    
      public void DoSomething() 
      {
        int repeatCount = 0
        while (repeatCount++ < MyClass.NumberOfRepeats)
        {
        }
      }
    }
    

    In your case you only have to let the Final<T> return the member value of the wrapped type as a read-only value. This means, the returned value itself must be immutable (an immutable reference type or struct) in case you want the returned instance to remain constant.

    However, the following example shows why your idea is redundant as you always end up assigning a copy of the value to a local variable while the local variable itself is never read-only:

    C#

    public readonly struct Final<T>
    {
      // Only works if T is value type
      public T Value => this.factory.Invoke();
      private Func<T> factory;
    
      public Final(Func<T> factory) 
      {
        // TODO::Consider to check if type is immutable using reflection in order to add robustness.
        this.factory = factory;
      }
    }
    
    class SomeMutableReferenceType
    {
      public SomeMutableReferenceType(double y)
      {
        this.Y = y;
      }
    
      public double Y { get; set; }
    }
    
    public void Main()
    {
      var mutableInstance = new SomeMutableReferenceType(12.0);
      var Final<SomeMutableReferenceType> finalValue 
        = new Final<SomeMutableReferenceType>(() => mutableInstance.Y);
    
      // Not allowed. Property will always return 12.0
      finalValue.Value = 3.0;
    
      // Allowed
      mutableInstance.Y = 3.0; 
    
      // Allowed!!!
      finalValue.Value = new Final<SomeMutableReferenceType>(() => mutableInstance.X);
    
      // However, the following code achieves the exact same behavior
      double value = mutableInstance.Y;
    
      // Then use value instead of mutableInstance.Y (effective final).
      // What makes your overall attempt redundant and only adds overhead.
      value = 3.0; // Does not change the original mutableInstance.Y
    }
    

    You may want to pass the read-only references to the consuming method by reference using the in keyword:

    C#

    public void Main()
    {
      var mutableInstance = new SomeMutableReferenceType(12.0);
    
      // Allowed
      mutableInstance = new SomeMutableReferenceType(3.0);
    
      // mutableInstance will never get modified by the following method
      DoSomethingWithReadOnlyVaariables(in mutableInstance);
    }
    
    private void DoSomethingWithReadOnlyVaariables(in SomeMutableReferenceType readOnlyValue)
    {
      // Compiler error.
      // This is the closes you can get to read-only locals in Java  
      var mutableInstance = new SomeMutableReferenceType(12.0);
    }
    

    Conclusion

    When looking at Java's final from a read-only or constant value point of view, the equivalent in C# would be the use of the in keyword and pass read-only variables to the consuming method. Of course, under the hoods both are completely different compiler features.

    When looking at performance, it is important to know that C# supports local constants. Java doesn't.
    Static compiletime values (constants) indeed offer performance benefits as they are allowed to be inlined by the compiler.

    A constant field or local variable have several advantages compared to final local variables (readability, maintainability, and static compiler optimizations) and makes final locals redundant (which only comes with a negligible performance gain - if any at all).

    When looking at final local variables from the perspective of the language context where they are mandatory to use (closures i.e. lambdas aka anonymous classes), then C# simply doesn't have those language limitations that require the introduction of such a keyword.

    As another matter of fact, it was discussed by the C# language design team to implement read-only locals. They again didn't see any value. What matters are the performance gains and those are negligible as read-only values are not static in terms of how they are interpreted by the compiler. For these special cases C# allows const locals.

    Most people agree that cluttering your code with a final or readonly keyword for every local variable does not improve the readability.
    And that following clean code principles usually leads to short compact methods that are easily understood and highly maintainable. Combine that with a carefully crafted set of unit tests and you are generally safe.

    Why final local variables in Java

    C# closures are superior to Java closures, therefore there is no requirement of a final pendant.

    Java didn't directly support pass-by-reference like C# does with its ref keyword. In Java you have to use "tricks" to achieve pass-by-reference.
    Java didn't support anonymous methods like C# does with the delegate keyword. This all leads to a different implementation of closures like lambdas.

    For Java that leads to some messy solutions in order to realize closures in Java. Because C# can directly support pass-by-reference, it is able to capture the local variable's reference.
    This means, in C# changing the value of the captured variable also changes the value within the capturing scope.
    In Java changing the value of the captured variable will not change the value within the capturing scope. In other words, Java closures work on copied values of variables (i.e. different memory locations) while C# closures work on copied variables (i.e. the same memory location).

    Of course, the Java way does not allow for robust programming. To solve this, they had to make captured local variables read-only, to guarantee that the original and captured value copy will never get out of sync.

    final local variables were introduced to overcome the design errors of the Java language and not to introduce read-only or constant variables. For this purpose, we have final and static final fields.
    If it wasn't for those langugae design errors I doubt Java would have final local variables.