CodeConverter icon indicating copy to clipboard operation
CodeConverter copied to clipboard

VB -> C#: Static variables are supposed to be lazy initialized inside method/property

Open jrmoreno1 opened this issue 3 years ago • 4 comments

VB.Net input code

Public Class VisualBasicClass

    Sub Main
        Console.WriteLine(Example)
    End Sub

    Public Readonly Property Example As Long 
        Get
            Static StaticVar As Long  = LongRunningFunction
            Return StaticVar -1 
        End Get
    End Property

    Public Shared Function LongRunningFunction As Long 
        Dim i  As Long = 1
        While i<10000000000
            i +=1
        End While  
    Return i
    End Function 
End Class

Erroneous output


public partial class VisualBasicClass
{
    public void Main()
    {
        Console.WriteLine(Example);
    }

    private long _Example_StaticVar = LongRunningFunction();

    public long Example
    {
        get
        {
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 10000000000L)
            i += 1L;
        return i;
    }
}

Expected output


public partial class VisualBasicClass
{
    public void Main()
    {
        Console.WriteLine(Example);
    }

        private long _Example_StaticVar;
	private bool _Example_StaticVar_Init;

    public long Example
    {
        get
        {
			if (!_Example_StaticVar_Init) {
				_Example_StaticVar = LongRunningFunction();
				_Example_StaticVar_Init=true;
			}
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 10000000000L)
            i += 1L;
        return i;
    }
}

Details

  • Website aka codeconverter.icsharpcode.net
  • Version in use: 8.4.7.0
  • Never saw it working
  • While Static variables aren't common, when using a static variable the lazy initialization is IMO essential. The "suggested" code isn't thread safe, unlike the VB code, but it's probably what most people would write as an equivalent. A more exact conversion of the Example property would be:
readonly Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag _Example_StaticVar_Init = new Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag();
 private long _Example_StaticVar;

public long Example
{
	get
	{
		bool InitStaticVariableHelper(Microsoft.VisualBasic.CompilerServices.StaticLocalInitFlag flag)
		{
			if (flag.State == 0)
			{
				flag.State = 2;
				return true;
			}
			else if (flag.State == 2)
			{
				throw new Microsoft.VisualBasic.CompilerServices.IncompleteInitialization();
			}
			else
			{
				return false;
			}
		}

		lock (_Example_StaticVar_Init)
		{
			try
			{
				if (InitStaticVariableHelper(_Example_StaticVar_Init))
				{
					_Example_StaticVar = LongRunningFunction();
				}
			}
			finally
			{
				_Example_StaticVar_Init.State = 1;
			}
		}
		return _Example_StaticVar;
	}
}

On one hand this is fairly close to an exact translation, on the other hand it's ugly. I would actually come down on the side of more exact translation (I do use it for the thread safety in a couple of places), but would understand if you felt it was too much, and the thread safety probably isn't all that important to most people (said without any basis other than a WAG).

jrmoreno1 avatar Apr 09 '22 06:04 jrmoreno1

Yeah that one is a slightly tricky decision. My sense is that most VB apps wouldn't use the thread safety, but obviously when it does matter it can be really annoying to track down a bug like this.

If there's a simple direct translation (or helper method we could introduce) to make the code look pretty simple inline, I think I'd be in favour of preserving laziness plus thread safety. In theory, Lazy<T> sounds useful for this, but it turns out that mutations to the static after initialization are supposed to be non thread-safe which leads me to this slightly clumsy idea:

public partial class VisualBasicClass
{
    public void Main()
    {
        System.Console.WriteLine(Example);
    }

    private System.Lazy<long> _Example_StaticVar_Lazy;
    private long _Example_StaticVar;
    
    public VisualBasicClass()
    {
        _Example_StaticVar_Lazy = new(() => _Example_StaticVar = LongRunningFunction());   
    }

    public long Example
    {
        get
        {
            var _ = _Example_StaticVar_Lazy.Value;
            _Example_StaticVar+=1; // If the input had StaticVar += 1 this would still work
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 10000000000L)
            i += 1L;
        return i;
    }
}

It's also possible to use the boolean overload of https://docs.microsoft.com/en-us/dotnet/api/system.threading.lazyinitializer?view=net-6.0#examples The advantage of that would be that the call to LongRunningFunction would be within a lambda at the same point it was in the VB

using static System.Threading.LazyInitializer;

public partial class VisualBasicClass
{
    public void Main()
    {
        System.Console.WriteLine(Example);
    }

    private object _Example_StaticVar_Lock;
    private bool _Example_StaticVar_IsInitialized;
    private long _Example_StaticVar;
    
    public long Example
    {
        get
        {
            EnsureInitialized(ref _Example_StaticVar, ref _Example_StaticVar_IsInitialized, ref _Example_StaticVar_Lock,
                () => LongRunningFunction()
            );
            _Example_StaticVar+=1; // If the input had StaticVar += 1 this would still work
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 10000000000L)
            i += 1L;
        return i;
    }
}

We could pull out a generic helper class in its own file once per project that tidies up the pattern a bit

using System;
using System.Threading;

public partial class VisualBasicClass
{
    public void Main()
    {
        System.Console.WriteLine(Example);
    }

    private LazyInitialization<long> _Example_StaticVar_Lazy = new();
    private long _Example_StaticVar;
    
    public long Example
    {
        get
        {
            _Example_StaticVar_Lazy.EnsureInitialized(ref _Example_StaticVar, () => LongRunningFunction());
            _Example_StaticVar+=1; // If the input had StaticVar += 1 this would still work
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 10000000000L)
            i += 1L;
        return i;
    }
}

public class LazyInitialization<T>
{
    private object _lock;
    private bool _isInitialized;
    public void EnsureInitialized(ref T toInitialize, Func<T> init) =>
        LazyInitializer.EnsureInitialized(ref toInitialize, ref _isInitialized, ref _lock, init);
}

GrahamTheCoder avatar Apr 09 '22 09:04 GrahamTheCoder

I like your last example better than the alternatives I had thought of (using the first example with a compiler error for the user to clear or using the lock version with the helper function moved out to make it slightly more readable).

It’s probably as clean as you can get without adding it C#, speaking of which I just mentioned it in the c# issue to do that: https://github.com/dotnet/csharplang/discussions/832

jrmoreno1 avatar Apr 09 '22 15:04 jrmoreno1

I've looked at that discussion and added to it with a slightly improved version of the above (SharpLab link):

using System;
using System.ComponentModel;
using System.Threading;


public sealed class LazilyInitialized<T>
{
    private T _value;
    private object _lock;
    private bool _isInitialized;
    public ref T EnsureInitialized(Func<T> init)
    {
        LazyInitializer.EnsureInitialized(ref _value, ref _isInitialized, ref _lock, init);
        return ref _value;
    }
}


public class SomeExampleClass
{
    public static void Main()
    {
        var instance = new SomeExampleClass();
        Console.WriteLine(instance.Counter);
        Console.WriteLine(instance.Counter);
    }    
    
    // Optional: To avoid cluttering up intellisense
    [EditorBrowsable(EditorBrowsableState.Never)]
    // Declared at the scope it exists in, but no-one can access the value without ensuring it's initialized
    private LazilyInitialized<long> _Example_StaticVar_Initializer = new();
    public long Counter
    {
        get
        {
            // Use whatever initialization logic you want here
            // It's lazy and executed at maximum 1 time per instance
            ref var _Example_StaticVar = ref _Example_StaticVar_Initializer.EnsureInitialized(() => LongRunningFunction());
            
            // Use of the variable otherwise is *not* thread safe (as in VB)
            _Example_StaticVar += 10;
            return _Example_StaticVar - 1L;
        }
    }

    public static long LongRunningFunction()
    {
        long i = 1L;
        while (i < 100000L)
            i += 1L;
        return i;
    }
}

GrahamTheCoder avatar Apr 22 '22 12:04 GrahamTheCoder

In my real code, the LongRunningFunction is sometimes a local function and sometimes a lambda, but I would expect that not to cause a problem with the translation, and even if it did, it would still be clearer what should be done (I try to keep the bus factor in mind and it might not be me doing the conversion).

For a translation that looks very good, the EditorBrowsable is a nice touch, not an exact equivalent to VB’s sugar to hide it entirety, but as you said it’s something that exists today and not in a few years.

jrmoreno1 avatar Apr 23 '22 16:04 jrmoreno1