假定以下情况:您有一个对象,该对象可以基于键(基本上是IDictionary<string, object>)存储任何对象。您想将不直接相关的各种类型的对象存储到其中。 (例如,字典可以是ASP.NET Session,也可以表示将序列化到磁盘以进行持久存储的字典。)

我不想创建一个单独的类来将包含所有这些对象,因为它们不是直接相关的,它们来自不同的地方。但是,如果分别存储每个对象,则意味着在获取某些值时必须使用强制类型转换,并且在设置它时不进行类型检查。

为了解决这个问题,我创建了一个泛型类型封装了密钥以及相关的类型和使用它的几种扩展方法:

class TypedKey<T>
{
    public string Name { get; private set; }

    public TypedKey(string name)
    {
        Name = name;
    }
}

static class DictionaryExtensions
{
    public static T Get<T>(this IDictionary<string, object> dictionary, TypedKey<T> key)
    {
        return (T)dictionary[key.Name];
    }

    public static void Set<T>(this IDictionary<string, object> dictionary, TypedKey<T> key, T value)
    {
        dictionary[key.Name] = value;
    }
}


用法:

private static readonly TypedKey<int> AgeKey = new TypedKey<int>("age");

…

dictionary.Get(AgeKey) > 18
dictionary.Set(AgeKey, age)


具有使用属性的类型安全性(同时在获取和设置上),同时由可存储任何内容的字典支持。

您如何看待这种模式?

评论

美丽的解决方案!

我将使用Convert.ChangeType而不是天真地转换为T。让TypeDescriptor框架为您处理对象转换!

#1 楼

您的建议并不是真正的类型安全,因为您仍然可以传递错误类型的密钥。因此,我只会使用普通的(字符串)键。但是我会添加一个考虑到类型的通用TryGet方法。设置器不必是通用的。

static class DictionaryExtensions
{
    public static T Get<T>(this IDictionary<string, object> dictionary, string key)
    {
        return (T)dictionary[key];
    }

    public static bool TryGet<T>(this IDictionary<string, object> dictionary,
                                 string key, out T value)
    {
        object result;
        if (dictionary.TryGetValue(key, out result) && result is T) {
            value = (T)result;
            return true;
        }
        value = default(T);
        return false;
    }

    public static void Set(this IDictionary<string, object> dictionary,
                           string key, object value)
    {
        dictionary[key] = value;
    }
}


然后您可以像这样使用字典。

int age = 20;
dictionary.Set("age", age);

// ...

age = dictionary.Get<int>("age");

// or the safe way
if (dictionary.TryGet("age", out age)) {
    Console.WriteLine("The age is {0}", age);
} else {
    Console.WriteLine("Age not found or of wrong type");
}


请注意,编译器可以推断出使用TryGet时使用泛型。



UPDATE

尽管有我上面的建议,我必须同意您的解决方案很优雅。这是另一个建议,它基于您的解决方案,但它封装了字典而不是提供密钥。好吧,它同时充当包装器和键。

public class Property<T>
{
    Dictionary<object, object> _dict;

    public Property (Dictionary<object, object> dict)
    {
        _dict = dict;
    }

    public T Value {
        get { return (T)_dict[this]; }
        set { _dict[this] = value; }
    }
}


或者,可以在属性的构造函数中提供一个字符串键。

您可以像这样使用它

private static readonly Dictionary<object, object> _properties = 
    new Dictionary<object, object>();
private static readonly Property<int> _age = new Property<int>(_properties);

...

_age.Value > 18
_age.Value = age


评论


\ $ \ begingroup \ $
问题在于,每次使用Get()时都必须指定类型,并且Set()可能使用错误的类型,这将在以后的Get()中导致运行时异常。但是您说对了,我的解决方案不是完全类型安全的。
\ $ \ endgroup \ $
– svick
2012年6月5日14:36

\ $ \ begingroup \ $
我再次选择了您的原始想法,并将其进一步发展。请查看我的更新。
\ $ \ endgroup \ $
–奥利维尔·雅科特·德ombes
2012年6月21日18:57

\ $ \ begingroup \ $
这肯定是一个有趣的想法。是的,我认为它比我最初提出的要好。尽管在我的情况下,将对象本身用作键(具有引用相等)将不起作用,因为字典是持久存在的。但是正如您提到的那样,可以很容易地对其进行修改。
\ $ \ endgroup \ $
– svick
2012年6月21日19:12

#2 楼

我知道您不想创建单个类,但这似乎正是需要的。我将创建一个新的班级,并喜欢写作。将整个蜡球称为PropertyBag,因为这样可以更清楚地表明其意图。我也是基于接口的开发的狂热者,因此我提取了其中的一些。请注意,一个构造函数重载需要一个非通用的IDictionary,因此您可以从任何现有字典(无论通用还是非通用)中创建一个。欢迎评论。

public interface ITypedKey<T>
{
    string Name { get; }
}

public class TypedKey<T> : ITypedKey<T>
{
    public TypedKey(string name) => this.Name = name ?? throw new ArgumentNullException(nameof(name));

    public string Name { get; }
}

public interface IPropertyBag
{
    T Get<T>(ITypedKey<T> key);

    bool TryGet<T>(ITypedKey<T> key, out T value);

    void Set<T>(ITypedKey<T> key, T value);

    void Remove<T>(ITypedKey<T> key);
}

public class PropertyBag : IPropertyBag
{
    private readonly IDictionary<string, object> _bag;

    public PropertyBag() => this._bag = new Dictionary<string, object>();

    public PropertyBag(IDictionary dict)
    {
        if (dict == null)
        {
            throw new ArgumentNullException(nameof(dict));
        }

        this._bag = new Dictionary<string, object>(dict.Count);
        foreach (DictionaryEntry kvp in dict)
        {
            this._bag.Add(new KeyValuePair<string, object>(kvp.Key.ToString(), kvp.Value));
        }
    }

    public T Get<T>(ITypedKey<T> key)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        return (T)this._bag[key.Name];
    }

    public bool TryGet<T>(ITypedKey<T> key, out T value)
    {
        if (this._bag.TryGetValue(key.Name, out object result) && result is T typedValue)
        {
            value = typedValue;
            return true;
        }

        value = default(T);
        return false;
    }

    public void Set<T>(ITypedKey<T> key, T value)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        this._bag[key.Name] = value;
    }

    public void Remove<T>(ITypedKey<T> key)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }

        this._bag.Remove(key.Name);
    }
}


评论


\ $ \ begingroup \ $
我的意思是我不想创建具有很多属性的单个类。您的方式确实有意义。
\ $ \ endgroup \ $
– svick
2012年6月5日14:38



#3 楼

我知道此解决方案确实有一些曲折的角落,并且大规模使用它会带来一些非常有趣的影响,但是请考虑将其作为思想实验。

在所有先前发布的示例中,您仍然拥有潜在的错误-即,某人可能有意或无意地执行以下操作:

private static readonly TypedKey<int> AgeKey = new TypedKey<int>("age");
private static readonly TypedKey<string> BadAgeKey = new TypedKey<string>("age");
dictionary.Set(BadAgeKey, “foo”);
...
// this would throw
dictionary.Get(AgeKey);


在编译时,您实际上无能为力验证一下。您可以实现某种代码分析器来查找AgeKeyBadAgeKey的示例。通常,这是通过名称间隔键解决的,以便它们不会重叠。

在此解决方案中,我尝试解决该问题……为此,我删除了字符串键,而是通过以下方式对字典进行了索引:类型,尤其是使用者用作键的类型,而不是所存储数据的类型。这样做意味着每个键必须是预定义的类型。它确实为您提供了访问数据的选项。

您可以使用Generic Type参数选择在编译时读取/写入的键,也可以通过将键作为常规参数传递来推迟运行时间。

然后我想到了:

public class TypeSafeKey<T> { }

public class TypeSafeKeyValuePairBag
{
    public T GetItemOrDefault<TKey, T>(T defaultValue = default(T)) where TKey : TypeSafeKey<T>
        => TryGet(typeof(TKey), out T result) ? result : defaultValue;

    public T GetItemOrDefault<T>(TypeSafeKey<T> key, T defaultValue = default(T))
        => TryGet(key?.GetType() ?? throw new ArgumentNullException(nameof(key)), out T result) ? result : defaultValue;

    public void SetItem<TKey, T>(T value) where TKey : TypeSafeKey<T>
        => m_values[typeof(TKey)] = value;

    public void SetItem<T>(TypeSafeKey<T> key, T value)
        => m_values[key?.GetType() ?? throw new ArgumentNullException(nameof(key))] = value;

    public T GetItem<TKey, T>() where TKey : TypeSafeKey<T>
        => Get<T>(typeof(TKey));

    public T GetItem<T>(TypeSafeKey<T> key)
        => Get<T>(key?.GetType() ?? throw new ArgumentNullException(nameof(key)));

    private bool TryGet<T>(Type type, out T value)
    {
        if (m_values.TryGetValue(type, out object obj))
        {
            value = (T)obj;
            return true;
        }

        value = default(T);
        return false;
    }

    private T Get<T>(Type type)
        => TryGet(type, out T result) ? result : throw new KeyNotFoundException($"Key {type.FullName} not found");

    private Dictionary<Type, object> m_values = new Dictionary<Type, object>();
}


然后使用它看起来像这样:

// You need to declare a Type for each key that you want to use, all though the types can defined anywhere
// They don't need to be known to the assembly where TypeSafeKeyValuePairBag is defined, but they do need to
// be known to the any code that is setting or getting any given key.  So even though the declaration of
// these class could be spread throughout the source tree, since each is a type they are forced to be unique
public class KeyHight : TypeSafeKey<int> { }
public class KeyWidth : TypeSafeKey<int> { }
public class KeyName : TypeSafeKey<string> { }

// A static class, with static public members would reduce the number of instances of objects that needed to be created for repeated reads/writes. 
// You would need to create these in a lazy fashion if you had many of them.  And since only their type matters, you don’t need to worry about locking, since two different instances of the same Type would function as the same key.  
public static class Keys
{
    public static KeyHight KeyHight { get; } = new KeyHight();
    public static KeyWidth KeyWidth { get; } = new KeyWidth();
    public static KeyName KeyName { get; } = new KeyName();
}

...

TypeSafeKeyValuePairBag bag = new TypeSafeKeyValuePairBag();

// Accessing hard coded keys
//Using Generic Type Parameters: The compiler can't infer the value Type from the Key Type, which means listing them both
bag.SetItem<KeyHight, int>(5);
//Passing the key as a parameter
bag.SetItem(Keys.KeyWidth, 10);
bag.SetItem(Keys.KeyName, "foo");

// Selecting which keys to access at run time
int value = 1;
foreach(var key in new TypeSafeKey<int>[] { Keys.KeyHight, Keys.KeyWidth })
{
    value *= bag.GetItem(key);
}

Console.WriteLine($"{bag.GetItem<KeyName, string>()}'s area is {value}");


这确实有一些大的缺点。具体来说,您需要创建很多类型来增加膨胀。并且序列化它会很冗长。使用错误的密钥仍然很容易,并且在可能导致运行时错误的地方使用错误的密钥很容易导致损坏。

如果您使用的是版本控制软件,那么使用类型会带来另一个很大的麻烦。在加载了不同程序集版本的混合过程中,可以轻松共享许多先前的示例。如果属性包中的所有项目都是原始类型,则绝对正确。版本1.0中定义的字符串常量“ foo”将与版本2.0中的字符串常量相同(假定没有人更改该值)。当您开始使用类型作为键时,如果有人使用一个键的版本来设置值,但是尝试使用另一个键来读取它,则将得到意想不到的结果,特别是1.0版中定义的KeyHight与2.0版中定义的KeyHeight类型不同

到达设置者和获取者的多种方法也可能使查找特定键的所有使用变得困难,但是由于它们是绑定类型的,因此大多数IDE都可以轻松地为您提供详尽的清单。访问。

我花了一些时间探索如果您具有这样的结构,还可以做什么。您可以利用继承来构建层次结构。可能有更简单,更有效的方法来执行此操作,但是请考虑是否要像下面这样更改TryGet方法:

private bool TryGet<T>(Type type, out T value)
{
    if (m_values.TryGetValue(type, out object obj))
    {
        value = (T)obj;
        return true;
    }

    Type baseType = type.BaseType;
    if (baseType != typeof(TypeSafeKey<T>))
    {
        return TryGet(baseType, out value);
    }

    value = default(T);
    return false;
}


您是这样使用它的: >
public class KeyLevel0Value : TypeSafeKey<int> { }
public class KeyLevelA1Value : KeyLevel0Value { }
public class KeyLevelA2Value : KeyLevelA1Value { }
public class KeyLevelB1Value : KeyLevel0Value { }
public class KeyLevelB2Value : KeyLevelB1Value { }

...

bag.SetItem<KeyLevelA1Value, int>(5);
// This will first check the value for LevelA2.  After not finding it, it will check LevelA2, and that value will be returned
Console.WriteLine(bag.GetItem<KeyLevelA2Value, int>());
// This will first check the value for LevelB2, LevelB1, and finally Level0, and since none are set it will return default
Console.WriteLine(bag.GetItemOrDefault<KeyLevelB2Value, int>());


因此,这确实创建了一个可以容纳任意类型的Property包,并且以类型安全的方式访问数据。

评论


\ $ \ begingroup \ $
这使我想起WPF的依赖项属性系统,除了它使用(静态只读)DependencyProperty实例作为键。使用TypeSafeKey <>实例而不是(派生的)类型作为键将提供相同级别的类型安全,而无需所有这些类。
\ $ \ endgroup \ $
– Pieter Witvoet
18年4月26日在8:31

#4 楼

我花了很长时间来解决这个问题。我终于得出结论,它实际上不是问题!
问题是,一旦将对象推送到集合中,就失去了编译时绑定。类型安全确实是一个编译时问题,而不是运行时问题。
我的理解是,当您创建泛型时,编译器会根据传递的泛型类型创建新的调用方法。因此,如果您有一个方法
,并使用
对其进行调用,则编译器将创建2个方法(我使用了伪代码签名为了便于访问。)
public void Foo<T>(){}

如果分配方法将这些方法聚合到集合中以进行检索,则编译器将无法知道从该集合中检索到的对象的类型。这样一来,您就不再具有类型安全性。
无论您试图跳过多少个环以使通用存储区成为未定义的类型,此操作的结果都是无法实现的。我得出的结论是,最好存储显式保存的类型,而不是创建实际上用作类型成员的重载,然后将类型显式存储在一个集合中,以便在运行时进行验证。生成的代码更精简,并且无需任何“模式”知识就可以理解。
我认为这是不可能的,但是需要对C#和一种或多种新的对象类型进行重大的语言更改才能方便它。我不愿意对此进行更正。
总结:
最好使用Dictionary 然后转换返回的对象。
这样您就可以
示例1
Foo<int>();

相比示例2
Foo<string>();

没有额外的类型安全性(如果试图访问错误的类型,两者都将编译,并且都将抛出运行时错误)。在示例2中,仅需维护其他代码。

#5 楼

通过使用类型转换器,可以在类型转换中提供更多的灵活性。

    public static T Get<T>(this IDictionary<string, object> dictionary, TypedKey<T> key)
    {
        return (T)Convert.ChangeType(dictionary[key.Name], typeof(T));
    }



static class DictionaryExtensions
{
    public static T Get<T>(this IDictionary<string, object> dictionary, TypedKey<T> key)
    {
        return (T)dictionary[key.Name];
    }

    public static void Set<T>(this IDictionary<string, object> dictionary, TypedKey<T> key, T value)
    {
        dictionary[key.Name] = value;
    }
}




编辑(在OP后面加上注释)

以下操作更有意义。它们可能是现有原始Get旁边的重载。

public static V Get<T, V>(this IDictionary<string, object> dictionary, TypedKey<T> key)
{
    return (V)Convert.ChangeType(dictionary[key.Name], typeof(T));
}

public static T Get<T>(this IDictionary<string, object> dictionary, string key)
{
    return (T)Convert.ChangeType(dictionary[key], typeof(T));
}


评论


\ $ \ begingroup \ $
我不明白,这有什么帮助?如果我有TypedKey ,我知道该值将始终为int,因此Convert.ChangeType根本没有帮助。
\ $ \ endgroup \ $
– svick
19年5月23日在18:22

\ $ \ begingroup \ $
@svick你是对的,我的意思是使用key类型为string的重载来实现这一点。不知道是否仍然可以更新我的帖子。
\ $ \ endgroup \ $
– dfhwze
19年5月23日在18:26

\ $ \ begingroup \ $
随时更新您的答案。在您收到答案后编辑问题的方法在这里不行,但事实并非如此。
\ $ \ endgroup \ $
– svick
19年5月23日在18:33

\ $ \ begingroup \ $
@svick我将保留我的第一篇文章,但添加您的评论。这样,我们就不会丢失有关该主题的任何历史记录;-)
\ $ \ endgroup \ $
– dfhwze
19年5月23日在18:34