2012年2月16日木曜日

C 系のビット操作と C# の enum

最近のプログラマはビット操作をやらないそうだ。
というより、毛の抜けた SE も今は滅多にやらなくなった。
そういう時代なのだろう。

だが、ビットをフラグとして扱うビット判定は今でもちょくちょく使う。
代表的な使用例が enum だ。以下 C# の例。

[FlagsAttribute]
enum AruFlags
{
    None   = 0x00, // 0000 0000
    One     = 0x01, // 0000 0001
    Two    = 0x02, // 0000 0010
    Three = 0x04, // 0000 0100
    Four  = 0x08, // 0000 1000
    All     = 0x0F // 0000 1111
};

enum は便利なので良く使う。
この enum にフラグ属性を持たせる場合は、上記のように宣言の前部に [FlagsAttribute] を加える。
それだけではダメで、中身の各 ID に対してビットフラグを定義する。
「うん?」
と思った人はプログラマだろう。
そう、何も [FlagsAttribute] を付けなくても、enum は中身は int なのだから、各 ID に対して 2 の y 乗を定義すれば良いんじゃない?
と思うだろう。実際、毛の抜けた SE は以前はそうしていた。
論より証拠、上記のコードから [FlagsAttribute] を取り去って、以下のようにビット判定しても何も問題は起きない。

AruFlags sonota = AruFlgas.One | AruFlags.Three;

if ((sonota & AruFlags.One) != 0) Console.WriteLine("One が入ってます");
if ((sonota & AruFlags.Two) != 0) Console.WriteLine("Two が入ってます");
if ((sonota & AruFlags.Three) != 0) Console.WriteLine("Three が入ってます");
if ((sonota & AruFlags.Four) != 0) Console.WriteLine("Four が入ってます");

/* 結果はもちろん以下のようになる
One が入ってます
Three が入ってます
*/

では何故、[FlagsAttribute] を加えるかというと、
MSDN リファレンスでは、
「列挙体をビット フィールド、つまりフラグのセットとして扱えることを示します。」
と要約していて、下部の解説で長々と説明しているが、あたかも、これを付けないとフラグ判定できませんよ、とも読める。
が、要するに、作り手(プログラマ)の礼儀作法を教育しているように思われる。
これは何も [FlagsAttribute] に限ったことではなく、最近の(以前から?) Microsoft の方針のようだ。
リファレンスの下部の解説部分でもそういう「ビットフィールドの作成方法」的な部分が大半を占める。

ということは、

「ビットフィールドとは何か」、
「ビットフィールドはどういう状況で使うのか」、
「ビットフィールドを作成・使用する際の注意点は何か」

という、昔は先輩のプログラマに教わったような初歩的な事柄が、

System.FlagsAttribute

という、enum でしか使わない特異で何だか怪しいものの解説ページに一緒に載っているのである。
もとい、大半を占めているのである。
異様な感じに包まれたのは、毛の抜けた SE だけだろうか。。。

しかし、違いはもちろんだが、ある。
FlagsAttribute のページではなく、列挙型のリファレンスにもあるように、
結果を文字列として表示するような場合、
例えば、

MessageBox.Show(sonota.ToString());

この結果が違ってくる。
[FlagsAttribute] 属性を付けると、「One, Three」と表示されるのに対して、
[FlagsAttribute] 属性を付けないと、「5」と表示される。


また、この [FlagsAttribute] 属性について、こんなコードを見つけた。(まんまではない)

[FlagsAttribute]
enum FileFlags
{
    None = 0,
    Read = 1,
    Write = 1 << 1,
    Create = 1 << 2,
    Delete = 1 << 3,
    All = 1 | 1 << 1 | 1 << 2 | 1 << 3
};

ビットフィールドを定義する際に、直に値を定義するのではなく、左シフト演算子を使って桁を上げていって、2 の y 乗を実現している。
懐かしくて、何だか心地よい気分になったもんだが、毛がフサフサなヒヨコにはどうか。
むしろ初心者は、2 のべき乗の値そのものを覚えておくべきだ。(せめて 1024 までは) というのが毛の抜けた SE の教育方針だ。



この enum 便利でよく使うが、あくまでも静的定義なので、VCL の 集合体 Set<> なんかみたいに、enum を組み込んで、Contains みたいなメソッドが使えないかなと思ってあれやこれや調べてたら、無い(笑)。
結局、Enum のクラスメソッドを使って実現するしか手はなさそう。
そして、作った関数がこれ。ID を文字列として検索して値を出力するのと、値を受け取って ID を文字列として返すもの。

public bool Search_Enum<TEnum>(string str, out int num)
{
    num = 0;
    try
    {
      var etype = typeof(TEnum);
      if (!etype.IsEnum) return false;
      if (Enum.GetUnderlyingType(etype) != typeof(int)) return false;
      foreach (int val in Enum.GetValues(etype))
      {
        if (str == Enum.GetName(etype, val))
        {
          num = val;
          return true;
        }
      }
      return false;
    }
    catch (Exception)
    {
      return false;
    }
}

public bool Get_Enum_Value<TEnum>(int num, out string str)
{
    str = "";
    try
    {
      var etype = typeof(TEnum);
      if (!etype.IsEnum) return false;
      if (Enum.GetUnderlyingType(etype) != typeof(int)) return false;
      if (!Enum.IsDefined(etype, num)) return false;
      str = Enum.GetName(etype, num);
      return true;
    }
    catch (Exception)
    {
     return false;
    }
}

う~ん、もっとスマートな方法は無いのかぇ?
しかし、これを作ったのにはもうひとつ理由があって、特に2つ目の関数がその目的なのだが、
enum の .ToString() って遅くない?
この関数つかっている部分と、.ToString() 使ってる部分とで、明らかに速度が違う。。。
何だかなぁ。。。

0 件のコメント:

コメントを投稿