Stupid Hack: Add / Remove only property in C#

UPDATE: The screenshot below was lost to the big /dev/null in a server move, so you have to use your imagination.

Can you make a property in C# that you can only add to and remove from? That is what an event does, but that only works with delegates. You can (ab)use an implicit conversion to get something like this:

class Foo
        {
            public delegate void FooDelegate();
            public string Message { get; set; }

            public Foo(string message)
            {
                Message = message;
            }

            public static implicit operator FooDelegate(Foo foo)
            {
                return foo.Dummy;
            }

            private void Dummy() { }
        }

        class Bar
        {
            public List<Foo> FooList { get; private set; }

            public event Foo.FooDelegate Foos
            {
                add
                {
                    FooList.Add((Foo)value.Target);
                }
                remove
                {
                    FooList.Remove((Foo)value.Target);
                }
            }

            public Bar()
            {
                FooList = new List<Foo>();
            }
        }

Using it:

static void Main(string[] args)
        {
            Bar bar = new Bar();
            bar.Foos += new Foo("Hello, World!");
            bar.Foos += new Foo("LOL!");
            foreach (Foo foo in bar.FooList)
                Console.WriteLine(foo.Message);
        }

It works – I wouldn’t recommend using it in production code though… Maybe more interresting is what would happen if we modified the CIL for an event to accept a non delegate type. Well let’s try it and see what happens!

The original C# code:

public class EventTester
    {
        private List<object> testList = new List<object>();
        public List<object> TestList { get { return testList; } }
        public event Action TestEvent
        {
            add
            {
                testList.Add(value);
            }
            remove
            {
                testList.Remove(value);
            }
        }
    }

Relevant parts of the generated CIL code:

.method public hidebysig specialname
                instance void add_TestEvent (
                        class [mscorlib]System.Action ‘value
                ) cil managed
        {
                // Method begins at RVA 0x2068
                // Code size 15 (0xf)
                .maxstack 8

                IL_0000: nop
                IL_0001: ldarg.0
                IL_0002: ldfld class [mscorlib]System.Collections.Generic.List`1<object> EventTest.EventTester::testList
                IL_0007: ldarg.1
                IL_0008: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<object>::Add(!0)
                IL_000d: nop
                IL_000e: ret
        } // end of method EventTester::add_TestEvent

        .method public hidebysig specialname
                instance void remove_TestEvent (
                        class [mscorlib]System.Action ‘value
                ) cil managed
        {
                // Method begins at RVA 0x2078
                // Code size 15 (0xf)
                .maxstack 8

                IL_0000: nop
                IL_0001: ldarg.0
                IL_0002: ldfld class [mscorlib]System.Collections.Generic.List`1<object> EventTest.EventTester::testList
                IL_0007: ldarg.1
                IL_0008: callvirt instance bool class [mscorlib]System.Collections.Generic.List`1<object>::Remove(!0)
                IL_000d: pop
                IL_000e: ret
        } // end of method EventTester::remove_TestEvent

        // Events
        .event [mscorlib]System.Action TestEvent
        {
                .addon instance void EventTest.EventTester::add_TestEvent(class [mscorlib]System.Action)
                .removeon instance void EventTest.EventTester::remove_TestEvent(class [mscorlib]System.Action)
        }

Let us replace “System.Action” with “System.Object” and see what happens. Running ilasm:

C:\Users\poizan\Documents\Visual Studio 2010\Projects\Test>ilasm /DLL EventTest.il

Microsoft (R) .NET Framework IL Assembler.  Version 4.0.30319.1
Copyright (c) Microsoft Corporation.  All rights reserved.
Assembling 'EventTest.il'  to DLL --> 'EventTest.dll'
Source file is ANSI

Assembled method EventTest.EventTester::get_TestList
Assembled method EventTest.EventTester::add_TestEvent
Assembled method EventTest.EventTester::remove_TestEvent
Assembled method EventTest.EventTester::.ctor
Creating PE file

Emitting classes:
Class 1:        EventTest.EventTester

Emitting fields and methods:
Global
Class 1 Fields: 1;      Methods: 4;
Resolving local member refs: 7 -> 7 defs, 0 refs, 0 unresolved

Emitting events and properties:
Global
Class 1 Events: 1;      Props: 1;
Resolving local member refs: 0 -> 0 defs, 0 refs, 0 unresolved
Writing PE file
Operation completed successfully

So far so good. Let us try to use it:

class Program
    {
        static void Main(string[] args)
        {
            EventTester et = new EventTester();
            et.TestEvent += "Hello, World!";
            et.TestEvent += "Magic!";
            foreach (string s in et.TestList.OfType<string>())
                Console.WriteLine(s);
            Console.ReadLine();
        }
    }

It builds successfully. And running it:

 And that is one more thing the C# compiler supports just fine but refuses to create.

Leave a Reply

Your email address will not be published. Required fields are marked *