VB -> C#: Static variables are supposed to be lazy initialized inside method/property
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).
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);
}
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
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;
}
}
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.