RAII Helperklassen in C#



  • Da es nicht so gut in den "C++: The good, the bad and the ugly" Thread passt, hier ein eigener Thread zur Diskussion von RAII Helperklassen in C# (bzw. auch gerne ähnlichen Konstrukten).

    Also DisposeGuard sieht so aus:

    public static class DisposeGuard
    	{
    		public static DisposeGuard<T> Alloc<T>(T target)
    			where T : class, IDisposable
    		{
    			bool complete = false;
    			try
    			{
    				var guard = new DisposeGuard<T>();
    				guard.Target = target;
    
    				complete = true;
    				return guard;
    			}
    			finally
    			{
    				if (!complete && target != null)
    					target.Dispose();
    			}
    		}
    	}
    
    	public class DisposeGuard<T> : IDisposable
    		where T : class, IDisposable
    	{
    		public T Target
    		{
    			get { return m_target; }
    			set { m_target = value; }
    		}
    
    		public DisposeGuard()
    		{
    		}
    
    		public T ReleaseTarget()
    		{
    			return Interlocked.Exchange(ref m_target, null);
    		}
    
    		public void Dispose()
    		{
    			var target = ReleaseTarget();
    			if (target != null)
    				target.Dispose();
    		}
    
    		private volatile T m_target;
    	}
    

    Verwendung dann z.B.

    public Stream GetCompressedStream()
    	{
    		var serializer = new JsonSerializer();
    
    		using (var memoryStreamGuard = DisposeGuard.Alloc(new MemoryStream()))
    		{
    			using (var gzipStream = new GZipStream(memoryStreamGuard.Target, CompressionLevel.Fastest, true))
    			using (var gzipStreamWriter = new StreamWriter(gzipStream, Encoding.UTF8))
    			{
    				serializer.Serialize(gzipStreamWriter, this);
    
    				gzipStreamWriter.Flush();
    				gzipStreamWriter.Close();
    			}
    
    			memoryStreamGuard.Target.Position = 0;
    
    			return memoryStreamGuard.ReleaseTarget();
    		}
    	}
    


  • Welche Vorteile hat das denn in C#? Ich zumindest sehe gerade keinen Sinnvollen Einsatzzweck den man nicht mit Standard-Mitteln auch gelöst bekommen würde.



  • hustbaer schrieb:

    using (var gzipStreamWriter = new StreamWriter(gzipStream, Encoding.UTF8))
    			{
    				serializer.Serialize(gzipStreamWriter, this);
    
    				gzipStreamWriter.Flush();
    				gzipStreamWriter.Close();
    			}
    

    Ist Flush und Close da wirklich nötig?



  • theugly schrieb:

    Ist Flush und Close da wirklich nötig?

    In dem Speziellen Fall nicht, da der Stream dannach ja sowieso Disposed wird.



  • Hallo hustbaer,

    Danke für den Code.
    Find ich richtig gut und werde ich auch beim meinem nächsten c#-Project nutzen.



  • In wiefern hilft der DisposeGuard denn überhaupt, außer dass er das ganze verkompliziert?

    Ein einfaches:

    using(MemoryStream stream = new MemoryStream())
    {
       //
    }
    

    würde es doch auch tun. Zumal du ja in deinem Beispiel plötzlich den StreamWriter genau so verwendest und keinen DisposeGuard einsetzt.

    Auch verstehe ich die Alloc<T>-Methode nicht. An welcher Stelle könnte es denn so knallen, dass das Try-Finally gerechtfertigt ist?



  • inflames2k schrieb:

    In wiefern hilft der DisposeGuard denn überhaupt, außer dass er das ganze verkompliziert?

    Da er den MemoryStream returned, kann er ihn ja nicht im using() einsetzen. Aber wenn er ihn ohne using erstellt und irgendwo eine exception fliegt, muss er aufräumen. Der Guard spart dir also ein try/catch zu schreiben.

    Wobei ich persönlich die komplexität in Alloc() gerade selber nicht verstehe - aber prinzipiell sind solche Guards durchaus praktisch.

    Schreib mal GetCompressedStream ohne den Guard 🙂



  • Gut, das mit dem Returnen des MemoryStreams hab ich jetzt übersehen. Alloc macht dennoch keinen Sinn.



  • Ich denke Alloc ist wohl so kompliziert, damit er sauber disposed wenn das new DisposeGuard() eine exception wirft. Was eigentlich nur in OOM situation passieren kann...



  • hustbaer schrieb:

    return Interlocked.Exchange(ref m_target, null);
    

    Brauchst du hier wirklich Thread-Safety? Es ist schliesslich allem voran ein Scope-Guard.



  • theugly schrieb:

    hustbaer schrieb:

    return Interlocked.Exchange(ref m_target, null);
    

    Brauchst du hier wirklich Thread-Safety? Es ist schliesslich allem voran ein Scope-Guard.

    Das Interlocked.Exchange hab' ich aus einer Klase übernommen wo es wirklich nötig ist, weil potentiell mehrere Threads das selbe Objekt mehrfach disposen können.

    Im DisposeGuard ist es wirklich unnötig.

    ----

    Über die komplizierte Alloc Funktion kann man geteilter Meinung sein (und ja, der Grund ist natürlich das new in der Funktion). Ich persönlich mag sowas einfach nicht unsauber schreiben wenn es auch relativ einfach sauber geht. Und da es so leicht ist wenn man nur 1x ScopeGuard.Alloc passend implementiert hat...


  • Administrator

    Wieso ist eigentlich das Target-Property nicht read only? Also so:

    public class DisposeGuard<T> : IDisposable
            where T : class, IDisposable
    {
        public T Target { get { return m_target; } }
    
        public DisposeGuard(T target)
        {
            m_target = target;
        }
    
        // ...
    }
    


  • Wieso sollte/müsste sie es sein?
    Ich müsste den Code durchsuchen ob ich den Setter irgendwo brauche, aber ich sehe einfach keinen Grund ihn wegzulassen.



  • hustbaer schrieb:

    Wieso sollte/müsste sie es sein?

    Aus meiner Sicht wäre es eher unsinnig, der Target-Property nachträglich etwas anderes zuzuweisen. Schließlich wird ein DisposeGuard ja nur über die Alloc-Methode erzeugt und kapselt hierbei ein bestimmtes Target.



  • Sehe ich ähnlich wie GPC. Denn wer stellt sicher, dass der vorherige Wert von Target wirklich korrekt Disposed wird?

    Man stelle sich folgendes überspitztes Beispiel vor:

    using(var guard = DisposeGuard.Alloc(new MemoryStream())
    {
        .. // irgendwelche arbeiten
        using(FileStream fs = File.Open(path, FileMode.Open)
        {
           guard.Target = fs;
           // irgendwelche arbeiten
        }   
    }
    

    Es ist so nicht mehr sichergestellt, dass der MemoryStream korrekt Disposed wird. - Soetwas würde ich sofort unterbinden.



  • Ja, man kann damit Unfug bauen.

    Man kann damit aber sowieso Unfug bauen, z.B. kann man immer ReleaseTarget zu früh aufrufen.

    Mein Ziel war nicht ein Hilfsmittel zu entwerfen das man möglichst nicht falsch verwenden kann, da ich der Meinung bin dass das hier gar nicht möglich ist.
    Mein Ziel war ein Hilfsmittel zu entwerfen das man möglichst einfach und möglichst flexibel korrekt verwenden kann. Und dazu gehört mMn. auch dass man das Target nachträglich zuweisen kann. Weil mMn. nicht viel Hirnschmalz dazugehört zu verstehen was für einen Effekt das hätte.

    Beispiel

    Foo Fun()
    {
        using (var fooGuard = new DisposeGuard<Foo>())
        {
            foreach (var stuff in m_stuff)
            {
                if (...)
                {
                    fooGuard.Target = new Foo(stuff);
                    break;
                }
            }
    
            if (fooGuard.Target == null)
                throw ...;
    
            DoSomeStuffWith(fooGuard.Target);
    
            return fooGuard.ReleaseTarget();
        }
    }
    

    Ich weiss, das kann man schöner lösen (z.B. über die LINQ Extension Methoden .Where und .First -- bzw. auch über eine eigene Hilfsfunktion), wobei dann auch die Notwendigkeit entfällt .Target nachträglich zuzuweisen. Ich wollte diese Art der Verwendung nur trotzdem nicht ganz ausschliessen.



  • Weiterer Anwendungsfall wo ich DisposeGuard häufig einsetze: Konstruktoren von IDisposable Klassen.

    class Foo : IDisposable
    {
        public Foo()
        {
            using (var disposeGuard = DisposeGuard.Alloc(this))
            {
                m_disposableResource1 = ...;
                m_disposableResource2 = ...;
                m_disposableResource3 = ...;
                disposeGuard.ReleaseTarget();
            }
        }
    }
    

  • Administrator

    @hustbear,
    Mir ging es vor allem darum zu verstehen, ob das irgendeinen Zweck verfolgt. Wenn es keinen Zweck hat, dann würde ich es weglassen. Eben weil es nicht benötigt wird, wie du es selbst schreibst. Wozu diese Zuweisungsmöglichkeit drin lassen, wenn man damit nur Unsinn machen kann?



  • Weil mir, wie ich schon geschrieben habe, Fälle einfallen wo man es doch braucht. Ob man die umschreiben kann so dass man es doch wieder nicht braucht, ist dann wieder ne andere Frage 🤡

    Aber egal. Meine Variante hat es drin. Wer die Klasse in eigene Projekte übernimmt kann den Setter ja weglassen - genau so wie das Interlocked.Exchange EDIT: und das volatile /EDIT.


  • Administrator

    hustbaer schrieb:

    Weil mir, wie ich schon geschrieben habe, Fälle einfallen wo man es doch braucht.

    Kannst du Beispiele nennen? Rein aus Interesse, weil mir keine einfallen.


Anmelden zum Antworten