Search code examples
c#.netwindowscocoa

Adding a static Create() method to a custom C# dictionary class


I've got a custom Dictionary implementation that covers a private Dictionary, and provides functionality closer to Apple's NSDictionary/NSMutableDictionary in the following two ways:

  1. Add() will not throw an exception if key is already present (old pair is removed before the add)
  2. dictionary["key"] will not throw an exception if the key is not present (instead default(TValue) is returned)

That's all working great and it's only a few lines of code:

public class NSDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
{
    private Dictionary<TKey, TValue> core = new Dictionary<TKey, TValue>();

    public TValue this[TKey key]
    {
        get { return this.ObjectForKey(key); }
        set { this.SetObjectForKey(value, key); }
    }

    public TValue ObjectForKey(TKey key)
    {
        try
        {
            return core[key];
        }
        catch (Exception exception)
        {
            return default(TValue);
        }
    }

    public void SetObjectForKey(TValue value, TKey key)
    {
        try
        {
            if ( core.ContainsKey(key) )
            {
                core.Remove(key);
            }

            core.Add(key, value);
        }
        catch (Exception exception)
        {
            // do nothing
        }
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return core.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)core).GetEnumerator();
    }
}

But now I'm stuck trying to figure out how to avoid exceptions during allocation/initialization by instead returning null on failure. C# constructors don't allow for this, so I have to add static factory methods ('factory' in the obj-c sense) that have this basic structure...

public static NSDictionary<TKey, TValue> Create()
{
    try
    {
        return new NSDictionary<TKey, TValue>();
    }
    catch (Exception exception)
    {
        return null;
    }
}

Pardon the not-exactly-on-point example, since the only way that 'new' allocation/initialization is going to fail is if we're out of memory, in which case a more heavy-handed approach like a thrown exception might not be the worst thing in the world.

So goofy example case aside, how would this be done, since calling code needs to know the key/value types and I haven't defined any way here to specify them?

I use this factory pattern throughout all of our C# projects for all of our custom classes, but this is the first time I've had to do it for a generic container class.

Weirdly, that method compiles, but the calling code does not:

NSDictionary<string, Version> dictionary = NSDictionary.Create();

I get:

Error CS0305 Using the generic type 'NSDictionary<TKey, TValue>' requires 2 type arguments

I think the crux of the issue here is that I can't figure out where the '<TKey, TValue>' type declarations should go in this static method situation.


Solution

  • since calling code needs to know the key/value types and I haven't defined any way here to specify them?

    Well you have. Since NSDictionary itself is a generic type, you need to specify its type arguments whenever you call a static method.

    // Create should probably take "params KeyValuePair<TKey, TValue>[]"?
    var dictionary = NSDictionary<string, Version>.Create();
    

    That said, I'm not sure why Create here would need to return null. As far as I know, the original NSDictionary in Objective-C doesn't have such an init. initWithObjects:forKeys: ignores any subsequent duplicate keys, instead of returning null or throwing an exception.

    If you want something similar to initWithObjects:forKeys: (though I don't recommend this, since you are writing C#), you can add an Add method, allowing your C# NSDictionary to use collection initialisers:

    public void Add(TKey, key, TValue value) 
    {
        if (!core.ContainsKey(key))
        {
            core.Add(key, value);
        }
    }
    

    Usage:

    var dictionary = new NSDictionary<string, string> {
        { "a", "b" },
        { "a", "c" }, // this will not be added
        { "b", "d" }
    };
    

    Also, as an aside, I would recommend not using exception handling to implement ObjectForKey and SetObjectForKey. You can just check ContainsKey to avoid the exception:

    public TValue ObjectForKey(TKey key)
    {
        if (core.ContainsKey(key))
        {
            return core[key];
        } 
        else 
        {
            return default;
        }
    }
    
    public void SetObjectForKey(TValue value, TKey key)
    {
        if (core.ContainsKey(key))
        {
            core[key] = value;
        } 
        else 
        {
            core.Add(key, value);
        }
    }