C
C#2w ago
hugeman

ValueType disposable scope

Is there a safe way to use a value type to manage a disposable "scope"? For example in this code, it should write "1" to the console, but it actually writes "2" because it creates a copy of scope1 and disposes it:
private static int disposeCount = 0;

public ref struct Scope(object myObject) {
private object? myObject = myObject;

public void Dispose() {
object? obj = myObject;
if (obj != null) {
myObject = null;
disposeCount++;
}
}
}

public static void Main() {
using (Scope scope1 = new Scope("some object")) {
Scope scope2 = scope1;
scope2.Dispose();
}

Console.WriteLine(disposeCount);
}
private static int disposeCount = 0;

public ref struct Scope(object myObject) {
private object? myObject = myObject;

public void Dispose() {
object? obj = myObject;
if (obj != null) {
myObject = null;
disposeCount++;
}
}
}

public static void Main() {
using (Scope scope1 = new Scope("some object")) {
Scope scope2 = scope1;
scope2.Dispose();
}

Console.WriteLine(disposeCount);
}
24 Replies
qqdev
qqdev2w ago
It's writing 2 because disposeCount is a global static field
hugeman
hugemanOP2w ago
I get that yeah
qqdev
qqdev2w ago
This would also happen with a class, for example
hugeman
hugemanOP2w ago
For a class it would only write 1 Since it swaps out myObject for null when disposed
qqdev
qqdev2w ago
true! Missed that
hugeman
hugemanOP2w ago
I'm just curious if there's a way to use a value type instead of a class since it's a somewhat hot path in my app
qqdev
qqdev2w ago
Is a copy of the struct a valid case in your app?
hugeman
hugemanOP2w ago
Nah
qqdev
qqdev2w ago
kk Just trying to get a more optimal solution I guess?
hugeman
hugemanOP2w ago
I guess i can just use Enter/Exit scope methods but then I have to use try finally everywhere
qqdev
qqdev2w ago
What are you doing with the object btw? That info may also help
hugeman
hugemanOP2w ago
It's to batch together multiple changes into a single operation Where each change by itself would trigger an expensive update, instead i can batch multiple changes
qqdev
qqdev2w ago
The the dispose is supposed to perform the single operation? kk, I like the pattern
hugeman
hugemanOP2w ago
If there's no other scopes also undisposed then yes it uses a counter
qqdev
qqdev2w ago
It kinda reminds me of the ILogger<T> interface from Microsoft Idk if the source of that is available but you might wanna take a look
hugeman
hugemanOP2w ago
They must use a class too since it returns IDisposable oh well i forgot you can make protected methods in an interface, I might try out something with that
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
you are calling Dispose twice there
hugeman
hugemanOP2w ago
Yes
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
no my bad it should work as is, why ref struct ?
hugeman
hugemanOP2w ago
I just copied the idea from the Lock class, i can't remember what it does except for disallow the value as a field in a non-ref struct
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
ref struct is useful when you need to group a bunch of ref T to something allows you to pass it around and refs remain valid
public static class StopwatchUtil
{
public struct StopwatchCtx : IDisposable
{
public Stopwatch sw;
public string name;
public bool disposed;
/// <summary>
/// Expecting only interned strings.
/// </summary>
/// <param name="name"></param>
public StopwatchCtx(string name)
{
this.name = name;
sw = Pool.stopwatch();
sw.Start();
}

public void Dispose()
{
if (disposed) return;
End();
}

public void End()
{
if (disposed) return;
float ms = sw.ElapsedMilliseconds;
Debug.Log($"[Miliseconds Passed] {ms,10} > {name}");
Pool.Return(ref sw);
name = null;
disposed = true;
}
}

public static StopwatchCtx Start(string name)
{
return new StopwatchCtx(name);
}
}
public static class StopwatchUtil
{
public struct StopwatchCtx : IDisposable
{
public Stopwatch sw;
public string name;
public bool disposed;
/// <summary>
/// Expecting only interned strings.
/// </summary>
/// <param name="name"></param>
public StopwatchCtx(string name)
{
this.name = name;
sw = Pool.stopwatch();
sw.Start();
}

public void Dispose()
{
if (disposed) return;
End();
}

public void End()
{
if (disposed) return;
float ms = sw.ElapsedMilliseconds;
Debug.Log($"[Miliseconds Passed] {ms,10} > {name}");
Pool.Return(ref sw);
name = null;
disposed = true;
}
}

public static StopwatchCtx Start(string name)
{
return new StopwatchCtx(name);
}
}
using (StopwatchUtil.Start("Data.LoadModels"))
{
var model_ctor = new DataFromTableConstructor(models, allPrototypes, null, null, data.models);
model_ctor.ConstructPhase();
model_ctor.LinkPhase();
}
using (StopwatchUtil.Start("Data.LoadModels"))
{
var model_ctor = new DataFromTableConstructor(models, allPrototypes, null, null, data.models);
model_ctor.ConstructPhase();
model_ctor.LinkPhase();
}
you can use an array like a stack and use a ref to the top of the stack
struct Scope : IDisposable
{
public void Dispose() { }
public static ScopeWrapper New()
{
return new ScopeWrapper();
}
}
ref struct ScopeWrapper : IDisposable
{
public Scope scope;
public ref Scope scoperef;

public ScopeWrapper()
{
scope = new Scope();
scoperef = ref Unsafe.AsRef(ref scope);
}
public void Dispose()
{
scope.Dispose();
}
}
static void Main(string[] args)
{
using (var scope = Scope.New())
{
ref Scope scopeRef = ref scope.scoperef;
}
struct Scope : IDisposable
{
public void Dispose() { }
public static ScopeWrapper New()
{
return new ScopeWrapper();
}
}
ref struct ScopeWrapper : IDisposable
{
public Scope scope;
public ref Scope scoperef;

public ScopeWrapper()
{
scope = new Scope();
scoperef = ref Unsafe.AsRef(ref scope);
}
public void Dispose()
{
scope.Dispose();
}
}
static void Main(string[] args)
{
using (var scope = Scope.New())
{
ref Scope scopeRef = ref scope.scoperef;
}
not safe at all but..
qqdev
qqdev2w ago
Ship it
eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
using (var scope = Scope.New())
{
scope.scoperef.Print();
using (var scope2 = Scope.New())
{
scope2.scoperef.Print();
}
}
using (var scope = Scope.New())
{
scope.scoperef.Print();
using (var scope2 = Scope.New())
{
scope2.scoperef.Print();
}
}

Did you find this page helpful?