【.NET】列挙型(enum)を拡張して文字列値を扱う

こんにちは、irislabのちひろです。

今回の話は、Microsoft .NET Framework(C#, VB.NET)のプログラムで列挙型(enum)を使用する際に、enumの値以外に文字列値を列挙型で管理したいというお話です。
正直なところ色々な方が試しているネタなので面白味には欠けますが、一応勉強ということで書いてみます。

まず、曜日を表す列挙型を書いてみますと下の例のようになります。
enum Days {Sat, Sun, Mon, Tue, Wed, Thu, Fri};

この列挙型から取得できる情報は0, 1, 2..., 6という整数値とToStringメソッドを使用することで取得できる"Sat”, "Sun", "Mon"..., "Fri"という文字列値になります。

しかしながら、実際のシステムで使用したいのは”土曜日”, “日曜日”, “月曜日”…, “金曜日”という日本語文字列だということが多々あります。
これを解決する方法としてパッと思いつくのは以下のような方法だと思います。

  1. 列挙型の要素定義で日本語文字列を使用する方法
  2. 列挙型の値を与えて日本語文字列を返す関数を定義する方法
  3. 列挙型の値をキーとして日本語文字列を格納するDictionary変数を用意する方法
  4. 列挙型の各要素にカスタム属性を付与して日本語文字列を管理する方法

少し前に私が友人から教わった方法は4の列挙型の各要素にカスタム属性を付与して日本語文字列を管理する方法でした。
その時のコードは無くなってしまいましたが、そのコードではFlags属性に対応していなかったのでそれに対応しつつ、拡張メソッド化して使いやすさを向上させています。

まずは日本語文字列を含めた列挙型の定義方法と使用方法です。

[C#]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using EnumExtension;
 
[Flags]
enum Days
{
    [EnumText("土曜日")]
    Sat = 1,
    [EnumText("日曜日")]
    Sun = 2
}
 
var value = Days.Sun;
Console.WriteLine(value.GetText());   // "日曜日"
value = Days.Sat | Days.Sun;
Console.WriteLine(value.GetText());   // "土曜日, 日曜日"

[VB.NET]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Imports EnumExtension
 
<Flags> _
Enum Days
    <EnumText("土曜日")> _
    Sat = 1
    <EnumText("日曜日")> _
    Sun = 2
End Enum
 
Dim value = Days.Sun
Console.WriteLine(value.GetText())   ' "日曜日"
value = Days.Sat Or Days.Sun
Console.WriteLine(value.GetText())   ' "土曜日, 日曜日"

いかがでしょうか。
列挙型と日本語文字列の定義位置が近いので、要素の増減に伴う修正漏れが発生しにくく、拡張メソッドを使用しているので使いやすくなっています。
また、Flags属性を付与している場合には、その値を構成する要素がカンマ区切りで取得できます。

さて、それを実現するための拡張メソッドとカスタム属性のコードです。
あまり説明するところがありませんが、ポイントとしてはFlags属性の有無をチェックしていることと、一度取得した日本語文字列をキャッシュする事で少しでも高速化できればというところでしょうか。
ちなみにキャッシュのキーには列挙型のインスタンスを使うことで、別の列挙型同士でも混じらずにキャッシュされます。int型とかにしてしまうと混じってしまうので注意が必要ですね。

[C#]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace EnumExtension
{
    [AttributeUsage(AttributeTargets.Field)]
    public class EnumText : Attribute
    {
        public EnumText(string text)
        {
            Text = text;
        }
        public string Text { get; set; }
    }
 
    public static class EnumExtension
    {
        private static Dictionary<Enum, string> _textCache = new Dictionary<Enum, string>();
 
        public static string GetText(this Enum instance)
        {
            lock (_textCache)
            {
                if (_textCache.ContainsKey(instance)) return _textCache[instance];
 
                var instanceType = instance.GetType();
 
                Func<Enum, string> enumToText = delegate(Enum enumElement)
                {
                    if (_textCache.ContainsKey(enumElement)) return _textCache[enumElement];
 
                    var attributes
                        = instanceType.GetField(enumElement.ToString()).GetCustomAttributes(typeof(EnumText), true);
                    if (attributes.Length == 0) return instance.ToString();
 
                    var enumText = ((EnumText)attributes[0]).Text;
                    _textCache.Add(enumElement, enumText);
 
                    return enumText;
                };
 
                if (Enum.IsDefined(instanceType, instance))
                {
                    return enumToText(instance);
                }
                else if (instanceType.GetCustomAttributes(typeof(System.FlagsAttribute), true).Length > 0)
                {
                    var instanceValue = Convert.ToInt64(instance);
 
                    var enumes =
                        from Enum value in Enum.GetValues(instanceType)
                        where (instanceValue & Convert.ToInt64(value)) != 0
                        select value;
 
                    var enumSumValue = 
                        enumes.Sum(value => Convert.ToInt64(value));
 
                    if (enumSumValue != instanceValue) return instance.ToString();
 
                    var enumText = string.Join(", ",
                        (from Enum value in enumes
                         select enumToText(value)).ToArray());
 
                    if (!_textCache.ContainsKey(instance))
                    {
                        _textCache.Add(instance, enumText);
                    }
 
                    return enumText;
                }
                else
                {
                    return instance.ToString();
                }
            }
        }
    }
}

[VB.NET]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
Imports System
Imports System.Collections.Generic
Imports System.Linq
 
Namespace EnumExtension
 
    <AttributeUsage(AttributeTargets.Field)> _
    Public Class EnumText
        Inherits Attribute
 
        Public Sub New(ByVal text As String)
            Me.Text = text
        End Sub
 
        Public Property Text As String
 
    End Class
 
    Public Module EnumExtension
 
        Private _textCache As New Dictionary(Of [Enum], String)()
 
        <System.Runtime.CompilerServices.Extension> _
        Public Function GetText(ByVal instance As [Enum]) As String
 
            SyncLock _textCache
 
                If _textCache.ContainsKey(instance) Then Return _textCache(instance)
 
                Dim instanceType = instance.GetType()
 
                Dim enumToText =
                    Function(enumElement As [Enum]) As String
 
                        If _textCache.ContainsKey(enumElement) Then Return _textCache(enumElement)
 
                        Dim attributes = instanceType.GetField(enumElement.ToString()).GetCustomAttributes(GetType(EnumText), True)
                        If attributes.Length = 0 Then Return instance.ToString()
 
                        Dim enumText = DirectCast(attributes(0), EnumText).Text
                        _textCache.Add(enumElement, enumText)
 
                        Return enumText
 
                    End Function
 
                If [Enum].IsDefined(instanceType, instance) Then
 
                    Return enumToText(instance)
 
                ElseIf instanceType.GetCustomAttributes(GetType(System.FlagsAttribute), True).Length > 0 Then
 
                    Dim instanceValue = Convert.ToInt64(instance)
 
                    Dim enumes =
                        From value As [Enum] In [Enum].GetValues(instanceType)
                        Where instanceValue And Convert.ToInt64(value) <> 0
                        Select value
 
                    Dim enumSumValue =
                        enumes.Sum(Function(enumElement) Convert.ToInt64(enumElement))
 
                    If enumSumValue <> instanceValue Then Return instance.ToString()
 
                    Dim enumText = String.Join(", ",
                        (From value As [Enum] In enumes
                         Select enumToText(value)).ToArray())
 
                    If Not _textCache.ContainsKey(instance) Then
                        _textCache.Add(instance, enumText)
                    End If
 
                    Return enumText
 
                Else
 
                    Return instance.ToString()
 
                End If
 
            End SyncLock
 
        End Function
 
    End Module
 
End Namespace

今後の課題としては、もう少しシンプルにしてキャッシュの使い方をもう少し考えようかな。
そのうち、改良版を載せるかもしれません。


関連する記事

カテゴリー: .Net タグ: , パーマリンク

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれないコメントは無視されますのでご注意ください。(スパム対策)