Proposal: Struct based RAII in C# (Copy constructor, Move constructor, Destructor) #4808
Replies: 14 comments 170 replies
-
IIRC the biggest problem came out of the lack of ownership (and transfer of ownership) semantics. Also see https://github.com/dotnet/roslyn/issues/160 |
Beta Was this translation helpful? Give feedback.
-
Because it is not necessary to have RAII. In modern frameworks, it is a bit uncommon to still require explicit dispose.
To me, there is more in resource management design. I have moved away from the traditional C++ way where individual objects manage resources and handling ownership transfer etc. Instead, the resources are better centrally managed as a service while most-used surrounding types can be dispose free. |
Beta Was this translation helpful? Give feedback.
-
I can imagine RAII only with struct like types - we could add a struct finalizer which would be implicitly called and could dispose all other resources. Perhaps it could be a new kind of type like what we have with |
Beta Was this translation helpful? Give feedback.
-
@msedi @HaloFour @MadsTorgersen @bernd5 @theunrepentantgeek I have update design for this proposal, now it looks more like in C++, but in C# style ;) |
Beta Was this translation helpful? Give feedback.
-
At least from my perspective and from the code I see at my daily work a few idioms, like the The code was following (not fully working, just an example) void ShowError()
{
using ViewModel model = new ViewModel();
ViewLocator.ShowWindow(model);
} If no one disposes the ViewModel (meaning an analyzer does not see someone calling Dispose) a warning is issued, that you either supress or ignore. I'm not sure if I want my codebase cluttered with At least for me what I really need is a deterministic finalizer on structs, and since it was asked to show an example, I can present one here (I already had a discussion with @YairHalberstadt) about this topic and he guided me to an older discussion of why its not so easy to do them, but that doesn't mean there is another solution. I have a IVolume that contains an array of (ISlice). Each ISlice contains data (an array of float e.g. 512x512). Additionally I have an IProcessor, that takes one or more IVolumes and produced another one or more IVolumes. But it is done in a pull based way, only when someone accesses the data an ISlice is created and filled with the slice data. Currently the ISlice has no IDisposable because it is a content of the IVolume and it does not make sense to allow to dispose the slice, while the IVolume exists. Each slice currently only returns a float[] array when calling GetData(). But what I want to have is to return an ReadOnlySpan where the data could either come from a memory mapped file region, from a heap allocated memory, from an ArrayPool or from a CUDA managed memory, and a few more. But, if it comes from e.g. an ArrayPool I need to return it, because the slice rented it from there. So I find ReadOnlySpan is maybe not enough, I would rather return an ISliceData with an implicit cast to ReadOnlySpan. But, and here comes the problem, should I put an IDisposable on every GetData() that returns the ISliceData? Before I didn't needed it because the GC has taken over. For performance reasons I would prefer the newer approach, but I have the drawback that it would be a breaking change and the older method is used more than 2000 times in the code. If yes, I can already see dramatic memory leaks because someone forgets to call IDisposable, no one warns you, and every now and then (depending on the system load) you can see OutOfMemoryExceptions and to find the leak in a highly parallel and non-determinstic processing chain of more than 100 processors running in parallel and ~200 volumes existing at the same time with T of data this will be a nightmare finding who is causing it. Putting a finalizer on it, and rely on the finalizer would create 100x200x10000 objects with finalizers attached to it, I have honestly no experience what this means, but ~ 200 millions and sometimes 6 billions of objects with finalizers would be created. At least I was told "take care with the amount of finalizers ;-)" and it would be non-deterministic since I don't know when the finalizers run and even then the GC does not know about my unmanaged objects (e.g. CUDA memory). Just my thoughts because this is my daily work... ;-) |
Beta Was this translation helpful? Give feedback.
-
Instead of adding RAII to simple structs, my suggestion is adding a new type of value type (which must be defined with an extra syntax like record struct) to support rust-style RAII, which has the following rules:
|
Beta Was this translation helpful? Give feedback.
-
The RAII topic bumps up every so often. I think there are reasons new generation of programming languages do not use it. Back in the days I still actively wrote C++, I often ask some RAII questions in an interview, and I found many candidates stuck on these questions:
And over the years, I forget the exact answers myself. And there were new special cases showed up from time to time. And yes, move semantics. And more new syntaxes for special case of move in later specs. So, I removed C++ from my toolbox. |
Beta Was this translation helpful? Give feedback.
-
I have created an example (not working completely), A small explanation, I have an interface that can keep many forms of a pointer (managed, unmanaged, from an arraypool, cuda, or some other memory that can be addressed with the pointer form). Since it is a pointer (a very simple variable, like a Span) I do not want an IDisposable on it is even not always necessary. If the // See https://aka.ms/new-console-template for more information
using System.Buffers;
using System.Runtime.InteropServices;
using var pointer = MemoryManager.Create<float>(10);
public unsafe interface IPointer<T> where T : unmanaged
{
Span<T> AsSpan();
}
public unsafe readonly struct PointerManaged<T> : IPointer<T> where T : unmanaged
{
private readonly T[] Data;
public PointerManaged(int size)
{
Data = GC.AllocateUninitializedArray<T>(size);
}
public Span<T> AsSpan() => Data;
}
public unsafe readonly struct PointerUnmanaged<T> : IDisposable, IPointer<T> where T : unmanaged
{
private readonly T* Ptr;
private readonly int Size;
public PointerUnmanaged(int size)
{
Size = size;
Ptr = (T*)Marshal.AllocHGlobal(size * sizeof(T));
}
public Span<T> AsSpan() => new(Ptr, Size * sizeof(T));
public void Dispose()
{
if (Ptr != null)
Marshal.FreeHGlobal((IntPtr)Ptr);
}
}
public unsafe readonly struct PointerArrayPool<T> : IDisposable, IPointer<T> where T : unmanaged
{
private readonly T[] Data;
public PointerArrayPool(int size)
{
Data = ArrayPool<T>.Shared.Rent(size);
}
public Span<T> AsSpan() => new(Data);
public void Dispose()
{
ArrayPool<T>.Shared.Return(Data);
}
}
static class CudaInterop
{
[DllImport("cuda.dll")]
internal static extern IntPtr cuMemAllocManaged(int size);
[DllImport("cuda.dll")]
internal static extern IntPtr cuMemFreeManaged(IntPtr size);
}
public unsafe readonly struct PointerCuda<T> : IDisposable, IPointer<T> where T : unmanaged
{
private readonly T* Ptr;
private readonly int Size;
public PointerCuda(int size)
{
Size = size;
Ptr = (T*)CudaInterop.cuMemAllocManaged(size * sizeof(T));
}
public Span<T> AsSpan() => new(Ptr, Size * sizeof(T));
public void Dispose()
{
if (Ptr != null)
CudaInterop.cuMemFreeManaged((IntPtr)Ptr);
}
}
public static class MemoryManager
{
public static IPointer<T> Create<T>(int size) where T : unmanaged
{
int condition = 0;
switch (condition)
{
case 0: return new PointerManaged<T>(size);
case 1: return new PointerUnmanaged<T>(size);
case 2: return new PointerArrayPool<T>(size);
case 3: return new PointerCuda<T>(size);
case 4: // stackalloc would be nice, doesn't work but thats another story.
default: throw new NotSupportedException();
}
}
} Years ago, when public readonly struct MySpan<T> : IDisposable where T : unmanaged
{
private readonly T[] Data;
public readonly int Length;
private readonly GCHandle handle;
public ref T this[int index] => ref Data[index];
public MySpan(T[] data)
{
Data = data;
Length = data.Length;
handle = GCHandle.Alloc(Data, GCHandleType.Pinned);
}
public void Dispose()
{
if (handle.IsAllocated)
handle.Free();
}
} |
Beta Was this translation helpful? Give feedback.
-
One additional thing why is this feature is useful - performance Everyone knows that Garbage Collector could decrease performance, to make it more deterministic and high performance we need deterministic way to allocate and deallocate object ... Even such languages like Rust allow to use deterministic destruction + garbage collector implementation in one application https://github.com/redradist/ferris-gc ... but C# offers only garbage collector ... For example destructors, move constructors and copy constructors could be added only for |
Beta Was this translation helpful? Give feedback.
-
@CyrusNajmabadi : You are so insisting on analyzers. It would be nice to have a survey if people would prefer a language change over an analyzer for a certain feature. Just telling us to solve it with analyzers goes in the same direction as saying we need proof for a language change. I agree that language changes should be done with care, but there are currently so many things that I cannot do with the language or with dramatic and unmaintainble efforts (e.g. generic math or generic casts). For example: public readonly ref struct ConvertedValue<TFrom, TTo> where TFrom:unmanaged where TTo:unmanaged
{
private readonly Span<TFrom> InternalValue;
public TTo this[int index]
{
[SkipLocalsInit]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
get
{
if ((typeof(TFrom)==typeof(byte)) && (typeof(TTo)==typeof(float)))
return (TTo)(object)(float)(int)(object)InternalValue[index];
else if ((typeof(TFrom) == typeof(sbyte)) && (typeof(TTo) == typeof(float)))
return (TTo)(object)(float)(sbyte)(object)InternalValue[index];
else if ((typeof(TFrom) == typeof(ushort)) && (typeof(TTo) == typeof(float)))
return (TTo)(object)(float)(ushort)(object)InternalValue[index];
else if ((typeof(TFrom) == typeof(short)) && (typeof(TTo) == typeof(float)))
return (TTo)(object)(float)(short)(object)InternalValue[index];
throw new NotSupportException();
}
}
public ConvertedValue(Span<TFrom> value)
{
InternalValue = value;
}
} At least for my team no one relies on analyzers and we even had severe performance drops and bugs introduced because the analyzer was applied. So currently people don't trust the analyers anymore. |
Beta Was this translation helpful? Give feedback.
-
To add to the conversation, why don't we just get what we want all the times in the sense of as long a feature is useful enough we can forget about the things we hate about it and make it optional. |
Beta Was this translation helpful? Give feedback.
-
I am working on a custom mapping solution for one of my projects, and the problem I'm facing is what to do when a field or property is a plain struct or class with no special attribute markings.
|
Beta Was this translation helpful? Give feedback.
-
• C / C++ : Manual memory management and a lot of ways to control how the memory is used and when is used. • C# : This style of programming, abstracts and hides all platform-dependent and machine-dependent details. However since the language has a lot of native code capabilities and interoperability features (unsafe code and raw pointers), these are supposed to be considered something extra, that allow some specific use-cases. Where it does matter most, is when there is a great need for more robust interoperability with native libraries (such as Silk Vulkan + OpenGL / SIMD Math / TensorFlow libraries etc) where you need to squeeze every possible percentage of perfomance. • Rust : A paradigm shift in memory management techniques, there is nothing to compare or contradict compared to C++, since is a different mental model. • C# Rustification : This can be considered as the ideal way to solve some of the shortcomings of Unsafe C#, but there is a contradictory nature here. Because Rust is concerned with memory safety and hence it tries to avoid all of C++ pitfals by design. C++ on the other hand is concerned about robust and efficient code (aka zero cost abstractions). In the context of C#, getting rustified is contradictory, because if you need safety you go for the features the DotNetRuntime offers. When you need to develop very robust and efficient libraries, you switch back to unsafe blocks and use raw pointers. |
Beta Was this translation helpful? Give feedback.
-
Yes, these all reasons why I propose this feature, but unfortunately mic-team is too close-minded ((( |
Beta Was this translation helpful? Give feedback.
-
Overview
One feature I miss a lot RAII like in C++
You will ask why ? C# already have
using
for resource ...using
for resources has one drawback it should be used only in on method, but what if I want:dispose
it deterministically !!Maybe it is time to add a
destructor
tostruct
?If C# would allow something like RAII it would be possible to implement zero-overhead abstractions (
std::shared_ptr
,std::unique_ptr
and etc.) + GC without using C++Design example
Below is taken
C++\Rust
approach for resource with RAII:This design introduce 2 new constructors and destructor for
struct
+ one extra keywordmove
Consider one extra example that will prevent coping of the struct:
Beta Was this translation helpful? Give feedback.
All reactions