查找或添加通用字典值的便捷方法

时间:2017-02-03 20:04:49

标签: c# generics

多年来,我多次需要代码:

  

在字典中查找值;如果它不存在,则将其添加到字典中(并返回该新值)。

例如:

    // Only one per account, so loading can be efficiently managed.
    // <AccountID, LCProfilePicture>
    public readonly static Dictionary<int, LCProfilePicture> All = new Dictionary<int, LCProfilePicture>();

    public static LCProfilePicture GetOrCreate( int accountID )
    {
        LCProfilePicture pic;
        if (!All.TryGetValue( accountID, out pic )) {
            pic = new LCProfilePicture( accountID );
            All[ accountID ] = pic;
        }

        return pic;
    }

我不想每次都写那个样板文件,而是希望有一个通用的方法来完成工作。怎么在c#中这样做?

到目前为止,我已经想到了三种方法:

  1. 如果字典尚未包含密钥对象,则将所需的构造包装到Action(或Func?)中。如有必要,请拨打电话。

  2. 要求 TValue拥有该表单的构造函数,然后以某种方式将该需求描述为泛型方法的约束。

  3. 定义interface必须满足的TValue,并以某种方式使用该接口编写泛型方法。

  4. 我想我知道如何做#1,所以一旦我弄清楚细节,就会提交答案。 更新:我现在已经解决了这个问题,并且贴出来作为答案。

    但也许#2可能吗?然后我可以添加该约束,并完成。

    • Pro:更容易使用(不必将构造包装成Action或Func)。
    • Con:不灵活(如果TValue没有这样的构造函数,就不能使用这种通用方法)。

    (#3看起来不太有希望;我提到完整性。)

4 个答案:

答案 0 :(得分:3)

您可以组合new()的约束和用于设置键的界面,如下所示:

interface IWithKey<T> {
    public T Key { get; set; }
}
static class DictExtensions {
    public static TVal GetorCreate<TKey,TVal>(this IDictionary<TKey,TVal> d, TKey key) where TVal : new(), IWithKey<TKey> {
        TVal res;
        if (!d.TryGetValue(key, out res)) {
            res = new TVal();
            res.Key = key;
            d.Add(key, res);
        }
        return res;
    }
}

由于GetorCreate是一个扩展名,您可以按如下方式使用它:

static LCProfilePicture GetOrCreatePic( int accountID ) {
    return All.GetOrCreateEntry(accountID);
}

答案 1 :(得分:1)

我在你的例子中注意到你有一个静态字典

// Only one per account, so loading can be efficiently managed.
// <AccountID, LCProfilePicture>
public readonly static Dictionary<int, LCProfilePicture> All = 
  new Dictionary<int, LCProfilePicture>();

我对此的第一反应是,因为它是静态的,你是否需要它是线程安全的。如果答案是肯定的,也许,甚至没有,那么答案可能是,不要自己写,让微软去做。

System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>

恰好有2个内置函数

TValue GetOrAdd(TKey key, TValue value)
TValue GetOrAdd(TKey key, Func<TKey, TValue> func)

所有这些都是以线程安全的方式完成的。 参数为Func的第二个可能是您正在寻找的答案。

如果您着手简化用法,我会认为反对将数据加载作为TValue的一部分。这主要是基于我自己的人选择存储POCO(普通旧CLR对象),因为值是字典而不是具有状态和行为的对象。

我会改为移动&#34;加载/构建/反序列化&#34;行为到另一个服务和/或词典本身。

此示例创建一个从

继承的基类
public abstract class SmartConcurrentDictionaryBase<TKey, TValue> : 
  System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>
{
  public TValue GetOrAdd(TKey key) { return GetOrAdd(key, LoadNewValue); }
  protected abstract TValue LoadNewValue(TKey key);
}

public class LCProfilePictureDictionary : SmartConcurrentDictionaryBase<int, LCProfilePicture>
{
  protected override LCProfilePicture(int accountID)
  { 
    return new LCProfilePicture(accountID);
  }
}

// use is
// var pic = All.GetOrAdd(accountID);

此示例更像是一个可重用的Dictionary对象,并将Func作为构造函数参数接收,但可以轻松更改为包含接口,其中接口上的某个函数与模式匹配。

public class SimpleConcurrentDictionary<TKey, TValue> :
  System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue>
{
  private readonly Func<TKey, TValue> _loadFunc;
  public SimpleConcurrentDictionary(Func<TKey, TValue> loadFunc)
  {
    _loadFunc = loadFunc;
  }
  public TValue GetOrAdd(TKey key) { return GetOrAdd(key, _loadFunc); }
} 

答案 2 :(得分:0)

解决方案#1(最常见):

public static TValue GetOrCreateEntry<TKey, TValue>( Dictionary<TKey, TValue> dict, TKey key, Func<TValue> creator )
{
    TValue value;
    if (!dict.TryGetValue( key, out value )) {
        value = creator();
        dict[ key ] = value;
    }

    return value;
}

使用示例:

static LCProfilePicture GetOrCreatePic( int accountID )
{
    return GetOrCreateEntry<int, LCProfilePicture>( All, accountID, () => new LCProfilePicture( accountID ) );
}

解决方案#2(对于记住密钥的电视节目):

public static TValue GetOrCreateEntry<TKey, TValue>( Dictionary<TKey, TValue> dict, TKey key, Func<TKey, TValue> creator )
{
    TValue value;
    if (!dict.TryGetValue( key, out value )) {
        value = creator(key);
        dict[ key ] = value;
    }

    return value;
}

使用示例:

static LCProfilePicture GetOrCreatePic( int accountID )
{
    return GetOrCreateEntry<int, LCProfilePicture>( All, accountID, key => new LCProfilePicture( key ) );
}

解决方案#1和解决方案2的比较:

解决方案#1更为通用 - 它甚至可以用于不需要了解密钥的电视节目。

解决方案#2是更清晰的风格,对于的TValues保留对密钥的引用。

在适当的情况下,最好有两个理由#2:

  • 原因#1:解决方案#1有可能被滥用:考虑TValue有两个构造函数,一个无参数构造函数和一个以密钥作为参数的构造函数的情况。没有经验的程序员可能会像这样使用解决方案#1:
static LCProfilePicture GetOrCreatePic( int accountID )
{
    // OOPS, programmer has not set the key field to "accountID".
    return GetOrCreateEntry<int, LCProfilePicture>( All, accountID, () => new LCProfilePicture() );
}

如果主程序员/架构师想要避免这种可能性,请省略解决方案#1,并仅提供#2。在这种情况下,尝试使用不会编译,因为没有匹配的构造函数。

  • 原因#2:使用解决方案#1需要在使用中包含密钥的第二个副本,如果TValue需要捕获它。这会不必要地将密钥封装在Func实例中,并可能导致意外引用不同的密钥,例如:
//...
int keyA;
int keyB;
// OOPS, programmer referred to the wrong key the second time.
// Maybe copy/pasted code, and only changed it in the first location, not realizing it is used in two places.
var valA = GetOrCreateEntry<int, LCProfilePicture>( All, keyA, () => new LCProfilePicture( keyB) );  
enter code here

答案 3 :(得分:0)

System.Reflection有一个ConstructorInfo对象和一个GetConstructor方法,可用于此目的。 ConstructorInfo.Invoke会返回您用于创建ConstructorInfo的类型的对象。如果你去反射路线,它看起来像这样(没有经过测试,但应该接近):

//using System.Reflection;
public static TValue GetOrCreateEntry<TKey, TValue>(Dictionary<TKey, TValue> dict, TKey key)
{
    TValue value;
    if (!dict.TryGetValue(key, out value))
    {
        // not in dictionary
        ConstructorInfo ctor = typeof(TValue).GetConstructor(new Type[] { typeof(TKey) });
        if (ctor != null)
        {
            // we have a constructor that matches the type you need
            value = (TValue)ctor.Invoke(new object[] { key });
            dict[key] = value;
            return value;
        }
        else
            throw new NotImplementedException(); // because the TValue type does not implement the constructor you anticipate
    }

    // we got it from dictionary, so just return it
    return value;
}
相关问题