ようこそ。睡眠不足なプログラマのチラ裏です。

FlagsAttributeとビット演算のちょっとしたレシピ

「ハッカーのたのしみ」はかなりの良書。いまさらFlagsAttributeのレシピ、リターンズ。.NET FrameworkにBitCountくらい標準であってもいいのにね
http://d.hatena.ne.jp/zecl/20100228/p1


※この記事の内容は古いです。上記の記事をあわせて読むことをオススメします。



C#クックブック第3版のレシピ12.12にて、「Flags属性を付与された列挙型の値の有効性の確認」
というレシピが掲載されていますが、今更感が否めないというか激しく既出というか・・・、
C#をそれなりに使いこなしている人にとっては、イマイチの内容ですね。


そんなわけで、Flags属性のビット演算に関して
例えばこんなレシピはどうでしょう。


その1:「Flags属性が付与された列挙型のうち、ビットがいくつ立っているのかを知る」
その2:「Flags属性が付与された列挙型のうち、ビットが立っているものを知る」


以下、コードです。

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace ClassLibrary1
{
    public static class Extentions
    {
        /// <summary>
        /// FlagsAttribute属性が付加されたenumについて、
        /// ビットが立っている要素数を取得します。
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="self"></param>
        /// <returns></returns>
        public static int GetEnumFlagsCount<T>(this T self) where T : struct
        {
            if (!self.GetType().IsEnum)
                throw new ArgumentException("emunじゃなきゃだめ");
            if (!Attribute.IsDefined(self.GetType(), typeof(FlagsAttribute)))
                throw new ArgumentException("FlagsAttrebute属性ついてなきゃだめ");

            var chars = Convert.ToString(Convert.ToInt32(self), 2);
            var count = 0;
            foreach (var m in Regex.Matches(chars, "1")) count++;
            return count;
        }

        /// <summary>
        /// FlagsAttribute属性が付加されたenumについて、
        /// ビットが立っているenumの要素コレクションを取得します。
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="self"></param>
        /// <returns></returns>
        public static IEnumerable<T> GetEnumFlags<T>(this T self) where T : struct
        {
            if (!self.GetType().IsEnum)
                throw new ArgumentException("emunじゃなきゃだめ");
            if (!Attribute.IsDefined(self.GetType(), typeof(FlagsAttribute)))
                throw new ArgumentException("FlagsAttrebute属性ついてなきゃだめ");

            var chars = Convert.ToString(Convert.ToInt32(self), 2);
            int count = chars.Length;
            foreach (var c in chars)
            {
                var flag = Convert.ToInt32(Math.Pow(2, --count));
                if (c == '1') yield return (T)Enum.ToObject(typeof(T), flag);
            }
        }
    }
}


実装はなんかダサダサな感じを漂わせていますが、要件は十分に満たしているかと。
もっとかっこいいコードが書けるよという人がいたら、ぜひ教えてください


使ってみるとこんな感じです。

using System;
using System.Linq;
using ClassLibrary1;

namespace ConsoleApplication1
{
    class Program
    {
        [Flags]
        enum EDocks
        { 
            Non    = 0x00000,
            Top    = 0x00001,
            Left   = 0x00002,
            Right  = 0x00004,
            Bottom = 0x00008,
            All    = EDocks.Top | EDocks.Left | EDocks.Right | EDocks.Bottom
        }

        static void Main(string[] args)
        {
            var enum0 = EDocks.Non;
            var enum1 = EDocks.Top;
            var enum2 = EDocks.Top | EDocks.Left;
            var enum3 = EDocks.Top | EDocks.Left | EDocks.Right;
            var enum4 = EDocks.All;
            var enum5 = EDocks.Non | EDocks.Left | EDocks.Left | EDocks.Non;

            Console.WriteLine(enum0.GetEnumFlagsCount());
            Console.WriteLine(enum1.GetEnumFlagsCount());
            Console.WriteLine(enum2.GetEnumFlagsCount());
            Console.WriteLine(enum3.GetEnumFlagsCount());
            Console.WriteLine(enum4.GetEnumFlagsCount());
            Console.WriteLine(enum5.GetEnumFlagsCount());
            Console.WriteLine();

            Console.WriteLine(Enum.ToObject(typeof(EDocks), enum4));
            Console.WriteLine();

            enum4.GetEnumFlags().ToList().ForEach(x => Console.WriteLine(x.ToString()));

            Console.WriteLine((16).GetEnumFlagsCount());
            Console.ReadLine();
        }
    }
}


実行結果

0
1
2
3
4
1

All

Bottom
Right
Left
Top

System.ArgumentException はハンドルされませんでした。
  Message="emunじゃなきゃだめ"
  Source="ClassLibrary1"
  StackTrace:
       場所 ClassLibrary1.Extentions.GetEnumFlagsCount[T](T self) 場所 D:\自作プログラム\FlagsAttrebuteのビットフラグのカウントとか\ConsoleApplication1\ClassLibrary1\Extentions.cs:行 19
       場所 ConsoleApplication1.Program.Main(String[] args) 場所 D:\自作プログラム\FlagsAttrebuteのビットフラグのカウントとか\ConsoleApplication1\ConsoleApplication1\Program.cs:行 42
       場所 System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       場所 System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       場所 Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       場所 System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       場所 System.Threading.ThreadHelper.ThreadStart()
  InnerException: 

enum以外の値型からも拡張メソッドが呼び出せてしまう点が、腑に落ちない感じはしますが、
そこはIsEnumとAttribute.IsDefinedでチェックして、例外投げるということで妥協。というか、
そもそもメソッド名を見た時点で、常識的に考えてそんな呼び方すんじゃねーよ!という感じではあるんですけど。