在C#中创建一个常量字典

160
什么是创建一个在运行时永不改变的字符串到整数映射的最有效方法?
我已经尝试使用const Dictionary,但是那样做并没有奏效。
我可以实现一个immutable wrapper以适当的语义,但这似乎还不完全正确。
对于那些询问过的人,我正在一个生成的类中实现IDataErrorInfo,并且正在寻找一种方法将columnName查找到我的描述符数组中。
当测试时我没有意识到(打了个错字!d'oh!),switch可以接受字符串,所以我会使用它。谢谢!

1
这里有一个解决方案: https://dev59.com/WGkw5IYBdhLWcg3wMHum - user2689819
11个回答

210

在C#中创建一个真正的编译时生成的常量字典并不是一项简单的任务。实际上,这里提供的答案都没有真正实现。

不过有一种解决方案可以满足您的需求,但并不一定很好;请记住,根据C#规范,switch-case表被编译为常量哈希跳转表。也就是说,它们是常量字典,而不是一系列if-else语句。因此,请考虑像这样的switch-case语句:

switch (myString)
{
   case "cat": return 0;
   case "dog": return 1;
   case "elephant": return 3;
}

这正是你所需要的。是的,我知道,它很丑。


13
无论如何,这是生成的代码,性能良好,可以在编译时计算,并且对于我的用例还具有其他良好的属性。我会按照这种方式来做! - David Schmitt
4
请注意,即使像这样返回非值类型也会破坏您的类的不可变性。 - Tom Anderson
41
switch-case很棒,但当你需要遍历值时就不太方便了。 - Alex from Jitbit
我来到这里是因为我认为我需要一个常量字典来帮助映射类之间的数据。相反,我需要的是一个包含开关的方法!此外,我现在可以将该方法要求在接口中,以供任何未来的数据映射类使用。这个答案非常准确。 - MrOodles
7
它缺少很多词典所拥有的好用功能。 - RomanKousta
1
@TomAnderson:如果返回的项是值类型(可变或不可变),或者它们是不可变类对象,则该结构将表现为不可变。 - supercat

41

当前框架中几乎没有不可变集合。我能想到一个相对无痛的选择是在.NET 3.5中使用Enumerable.ToLookup() - Lookup<,>类是不可变的(但rhs上是多值的);你可以很容易地从Dictionary<,>中实现这一点:

    Dictionary<string, int> ids = new Dictionary<string, int> {
      {"abc",1}, {"def",2}, {"ghi",3}
    };
    ILookup<string, int> lookup = ids.ToLookup(x => x.Key, x => x.Value);
    int i = lookup["def"].Single();

18
enum Constants
{
    Abc = 1,
    Def = 2,
    Ghi = 3
}

...

int i = (int)Enum.Parse(typeof(Constants), "Def");

2
有趣的想法!我在想Parse()调用的性能如何。我担心只有分析器才能回答这个问题。 - David Schmitt

17

我不确定为什么没有人提到这一点,但在C#中,对于那些我无法分配const的东西,我使用静态只读属性。

例如:

public static readonly Dictionary<string, string[]> NewDictionary = new Dictionary<string, string[]>()
        {
            { "Reference1", Array1 },
            { "Reference2", Array2 },
            { "Reference3", Array3 },
            { "Reference4", Array4 },
            { "Reference5", Array5 }
        };

5
因为字典的内容仍然可变,并且它是在运行时分配的。 - David Schmitt

11

这是你可以获得的最接近于“常量字典”的东西:

public static int GetValueByName(string name)
{
    switch (name)
    {
        case "bob": return 1;
        case "billy": return 2;
        default: return -1;
    }
}
编译器会智能地构建尽可能干净的代码。

9

如果使用4.5+框架,我会使用ReadOnlyDictionary(对于列表也是ReadOnly Collection)来进行只读映射/常量。它的实现方式如下:

static class SomeClass
{
    static readonly ReadOnlyDictionary<string,int> SOME_MAPPING 
        = new ReadOnlyDictionary<string,int>(
            new Dictionary<string,int>()
            {
                { "One", 1 },
                { "Two", 2 }
            }
        )
}        

私有静态只读字典<string,string> _yourDictionaryName = new Dictionary<string,int>(StringComparer.OrdinalIgnoreCase) { { "One",1 }, { "Two",2 }, { "Three",3 }, }; 这是我的做法。 - Sanket Sonavane
@SanketSonavane 一个 readonly Dictionary<TKey, TValue> 并不等同于一个 ReadOnlyDictionary<TKey, TValue>。实际上,它们的“只读”方式几乎是相反的。请参见:https://dotnetfiddle.net/v0l4aA。 - Broots Waymb

3

为什么不使用命名空间或类来嵌套您的值?虽然它可能不完美,但非常干净。

public static class ParentClass
{
    // here is the "dictionary" class
    public static class FooDictionary
    {
        public const string Key1 = "somevalue";
        public const string Foobar = "fubar";
    }
}

现在你可以访问.ParentClass.FooDictionary.Key1等内容。

因为它没有实现IDataErrorInfo接口。 - David Schmitt

3

从C# 8开始,新的switch表达式是实现这一结果最简洁的方式:

int value = inputString switch {
    "one" => 1,
    "two" => 2,
    _ => -1
};

作为函数使用
int GetValue(string inputString) => inputString switch {
    "one" => 1,
    "two" => 2,
    _ => -1
};

这个版本比“旧”版本更加简洁:

int value = -1;
switch (inputString){
    case "one": value=1; break;
    case "two": value=2; break;
}

旧版本和新版本的性能差异可能因不同编译器而异。


这种写法确实更整洁,但你能解释/展示一下相比于普通的 switch 语句,它在 CPU 时间上有多快吗?我认为(!)一个好的(!)编译器可以为你的两个示例创建完全相同的 CLR 指令。 - David Schmitt
@DavidSchmitt 你说得对,我是在使用Codewars的默认编译器时得出了关于速度的结论,当我在Linqpad中重新运行相同的内容时,并没有显著的差异。 - PawZaw

2

由于我要绑定到一个WinForms组合框,所以这只是另一个想法:

public enum DateRange {
    [Display(Name = "None")]
    None = 0,
    [Display(Name = "Today")]
    Today = 1,
    [Display(Name = "Tomorrow")]
    Tomorrow = 2,
    [Display(Name = "Yesterday")]
    Yesterday = 3,
    [Display(Name = "Last 7 Days")]
    LastSeven = 4,
    [Display(Name = "Custom")]
    Custom = 99
    };

int something = (int)DateRange.None;

从显示名称获取int值:

public static class EnumHelper<T>
{
    public static T GetValueFromName(string name)
    {
        var type = typeof(T);
        if (!type.IsEnum) throw new InvalidOperationException();

        foreach (var field in type.GetFields())
        {
            var attribute = Attribute.GetCustomAttribute(field,
                typeof(DisplayAttribute)) as DisplayAttribute;
            if (attribute != null)
            {
                if (attribute.Name == name)
                {
                    return (T)field.GetValue(null);
                }
            }
            else
            {
                if (field.Name == name)
                    return (T)field.GetValue(null);
            }
        }

        throw new ArgumentOutOfRangeException("name");
    }
}

使用方法:

var z = (int)EnumHelper<DateRange>.GetValueFromName("Last 7 Days");

1

似乎没有标准的不可变字典接口,因此创建包装器似乎是唯一合理的选择,不幸的是。

编辑:Marc Gravell找到了我错过的ILookup - 这将允许您至少避免创建新的包装器,尽管您仍然需要使用.ToLookup()转换字典。

如果这是特定场景下的需求限制,您可能最好使用更面向业务逻辑的接口:

interface IActiveUserCountProvider
{
    int GetMaxForServer(string serverName);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接