C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

フラグを操作するジェネリックメソッドを作る方法

[トピック内 11 記事 (1 - 11 表示)]  << 0 >>

■86903 / inTopicNo.1)  フラグを操作するジェネリックメソッドを作る方法
  
□投稿者/ MTK (32回)-(2018/04/02(Mon) 16:12:15)

分類:[C#] 

お世話になります。

クラスAの現在の状態を管理するため、管理用の変数を作ってフラグによるビット演算を行っています。
下記は現在の状態です



------------------  フラグデータクラス  ------------------

// クラスA用のフラグ
[Flags]
public enum FlagEnumA
{
  Zero = 0,

  Visible = 1,

  Enable = 1 << 1,

  Save = 1 << 2
}



------------------  フラグ操作クラス  ------------------

// 指定されたフラグを加算するメソッド
public static void AddFlag(ref FlagEnumA flag, FlagEnumA state)
{
  flag = flag | state;
}




この状態で、新たにクラスBの状態を管理する必要が出てきました。
そこでクラスB用の FlagEnumB を作り、フラグ操作クラスにある AddFlag メソッドをジェネリックにしようと考えています。
しかしこのままジェネリックにすると制約がないため flag = flag | state の部分でエラーが出てしまいます。
Whereを加えることで制約を付けることができるとあったのですが、この場合はどのような制約をかければ対応ができるのでしょうか?

引用返信 編集キー/
■86908 / inTopicNo.2)  Re[1]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ Azulean (942回)-(2018/04/02(Mon) 23:12:43)
ジェネリックではできないかもしれません。

No86903 (MTK さん) に返信
> // 指定されたフラグを加算するメソッド
> public static void AddFlag(ref FlagEnumA flag, FlagEnumA state)
> {
>   flag = flag | state;
> }

この例だと、メソッドにせず、単なる演算式として |= や | で良いのでは?と感じてしまいます。
サンプルだとうまく示せていない、何か(メリット)があるのでしょうか?
(ref で引き渡せると言うことは、フィールドとして隠蔽されているわけでもなさそうなため)
引用返信 編集キー/
■86909 / inTopicNo.3)  Re[2]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ MTK (33回)-(2018/04/03(Tue) 10:25:14)
No86908 (Azulean さん) に返信

回答ありがとうございます。
なるほど、ジェネリックではできなさそうなんですね。


必ずメソッドでなければならない理由はありませんので、演算式で対応しようと思います。

このフラグ加算のメソッド以外にもフラグ除外やフラグ有無チェックのメソッドなどがあり、
演算式ではなくメソッドにしたのはこれらの機能をまとめた汎用クラスを作っておけば楽かなと思ったためです。
当初の予定では FlagEnumA のみを使ってクラスAの管理用変数とクラスBの管理用変数を操作する予定だったので
refで管理用変数を引き渡せるように設計しておりました。
解決済み
引用返信 編集キー/
■86918 / inTopicNo.4)  Re[3]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ Azulean (943回)-(2018/04/04(Wed) 06:33:35)
No86909 (MTK さん) に返信
> このフラグ加算のメソッド以外にもフラグ除外やフラグ有無チェックのメソッドなどがあり、
> 演算式ではなくメソッドにしたのはこれらの機能をまとめた汎用クラスを作っておけば楽かなと思ったためです。

どうしてもメソッドにしておきたい場合は、T4 Template の検討をするところですね。
これであれば、機械的にコードを生成できるので。

解決済み
引用返信 編集キー/
■86924 / inTopicNo.5)  Re[4]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ MTK (37回)-(2018/04/04(Wed) 10:03:13)
No86918 (Azulean さん) に返信

T4 Template というのは初めて聞きましたが、これは便利そうですね!
今回に限らず色々と使えそうです。
教えて頂いてありがとうございます。
解決済み
引用返信 編集キー/
■86934 / inTopicNo.6)  Re[1]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ 魔界の仮面弁士 (1600回)-(2018/04/04(Wed) 11:57:49)
No86903 (MTK さん) に返信
> Whereを加えることで制約を付けることができるとあったのですが、この場合はどのような制約をかければ対応ができるのでしょうか?

Where ではなく where ですね。

列挙型は型制約として指定できないので、struct で代用しておいた上で
さらにメソッド内で Type.IsEnum による型チェックを施す感じでしょうか。


public static class BitFlagHelper
{
 public static void SetFlag<TEnum>(ref this TEnum self, TEnum flag) where TEnum : struct
 {
  System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum);
  var bits = Convert.ToUInt64(self) | Convert.ToUInt64(flag);
  self = (TEnum)System.Enum.ToObject(typeof(TEnum), bits);
 }
}


> // 指定されたフラグを加算するメソッド
> public static void AddFlag(ref FlagEnumA flag, FlagEnumA state)

プロパティが公開されているなら、そのまま
 objClassA.PropA |= FlagEnumA.Enable;
の方が素直だとは思います。

しかし今回は、ビットフラグの操作に慣れていない利用者に向けて、
 FlagOperator.AddFlag(ref objClassA.PropA, FlagEnumA.Enable);
のようなメソッドを作りたい…と事情であろうかと推察します。


しかし、ジェネリックを使うかどうかは抜きにしても、そもそも
C# では、ref や out な引数にプロパティを渡せない仕様なので、
残念ながらそういうことはできません。


たとえば上記の BitFlagHelper を呼ぶにしても、
  BitFlagHelper.SetFlag(ref objClassA.PropA, FlagEnumA.Visible);
のような呼び出しは許可されていないので、受け渡し用の変数が必要になってしまいます。

  var propA = objClassA.PropA; // ref で渡すための変数
  //「propA.SetFlag(FlagEnumA.Visible);」でも可
  BitFlagHelper.SetFlag(ref propA, FlagEnumA.Visible);
  objClassA.PropA = propA;



Visual Basic であれば、ByRef 引数へのプロパティ渡しが禁止されていないので、上記を
  'objA.PropA = objA.PropA Or FlagEnumA.Visible
  BitFlagHelper.AddFlag(objA.PropA, FlagEnumA.Enable)
のように呼び出せるのですけれどね…。



> これらの機能をまとめた汎用クラスを作っておけば楽かなと思ったためです。
ジェネリック クラスのかわりに、拡張メソッドを使うのはどうでしょうか。

たとえば DateTime 型の Add 何某メソッドは、「日付の加減算結果を返す」メソッドであり、
  dateTimePicker1.Value = dateTimePicker1.Value.AddDays(1); // 翌日の日付に変更
などのように書けますが、それを真似て
 objClassA.PropA = objClassA.PropA.SetFlag(FlagEnumA.Visible); // Visible フラグを立てる
などと書ける様な SetFlag 拡張メソッドを追加するなど。


あるいは汎用化に拘らず、クラスA 自身に
 public bool this[FlagEnumA flag]
 {
  get { return this.PropA.HasFlag(flag); }
  set { if (value) { this.PropA |= flag; } else { this.PropA &= ~flag; } }
 }
のようなインデクサを用意して、利用する側から
 objClassA[FlagEnumA.Visible] = true;
のようにしてビットフラグを読み書きできるようにしておくとか。
今回の目的からは離れてしまいますけれども。
解決済み
引用返信 編集キー/
■86937 / inTopicNo.7)  Re[2]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ MTK (40回)-(2018/04/04(Wed) 14:38:54)
No86934 (魔界の仮面弁士 さん) に返信

回答ありがとうございます。


> Where ではなく where ですね。
whereですね、失礼しました。




> 列挙型は型制約として指定できないので、struct で代用しておいた上で
> さらにメソッド内で Type.IsEnum による型チェックを施す感じでしょうか。
>
> public static class BitFlagHelper
> {
>  public static void SetFlag<TEnum>(ref this TEnum self, TEnum flag) where TEnum : struct
>  {
>   System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum);
>   var bits = Convert.ToUInt64(self) | Convert.ToUInt64(flag);
>   self = (TEnum)System.Enum.ToObject(typeof(TEnum), bits);
>  }
> }

すいません 例で出して頂いたプログラムを調べていますが、まだちゃんと理解できていません;;
型制約をstructで指定しておいて、メソッド内で型チェックをする という理屈は理解できていると思います。
お時間があれば、少し教えて頂けたらと思います。


> public static void SetFlag<TEnum>(ref this TEnum self, TEnum flag) where TEnum : struct
ref this TEnum self  というように this を使っているということはこれは後述されている拡張メソッドなのでしょうか?


> System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum);
これは型が TEnumかチェックを行い、違っていたらメッセージボックスで呼び出し履歴を表示ですかね


> var bits = Convert.ToUInt64(self) | Convert.ToUInt64(flag);
論理和を使えるように、受け取った変数とフラグをUInt64に変換している? この場合、bitsはUInt64として扱われるのでしょうか?


> self = (TEnum)System.Enum.ToObject(typeof(TEnum), bits);
UInt64型になっている bits を TEnum型に変換し、受け取った変数に保存している?




> しかし、ジェネリックを使うかどうかは抜きにしても、そもそも
> C# では、ref や out な引数にプロパティを渡せない仕様なので、
> 残念ながらそういうことはできません。

ref や out な引数にはプロパティを渡せないんですね。
勉強不足でした。
変数を経由して渡せば、実装は可能なのですね。




> ジェネリック クラスのかわりに、拡張メソッドを使うのはどうでしょうか。
>
> たとえば DateTime 型の Add 何某メソッドは、「日付の加減算結果を返す」メソッドであり、
>   dateTimePicker1.Value = dateTimePicker1.Value.AddDays(1); // 翌日の日付に変更
> などのように書けますが、それを真似て
>  objClassA.PropA = objClassA.PropA.SetFlag(FlagEnumA.Visible); // Visible フラグを立てる
> などと書ける様な SetFlag 拡張メソッドを追加するなど。

こんなやり方があるんですね。
やや冗長になってはしまいますが、汎用的に使えるのは魅力ですね!




> あるいは汎用化に拘らず、クラスA 自身に
>  public bool this[FlagEnumA flag]
>  {
>   get { return this.PropA.HasFlag(flag); }
>   set { if (value) { this.PropA |= flag; } else { this.PropA &= ~flag; } }
>  }
> のようなインデクサを用意して、利用する側から
>  objClassA[FlagEnumA.Visible] = true;
> のようにしてビットフラグを読み書きできるようにしておくとか。
> 今回の目的からは離れてしまいますけれども。

インデクサって使ったことがなかったですが、こんな風に活用できるんですね。
勉強になります。
ありがとうございました。
解決済み
引用返信 編集キー/
■86940 / inTopicNo.8)  Re[3]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ 魔界の仮面弁士 (1601回)-(2018/04/04(Wed) 16:01:40)
No86937 (MTK さん) に返信
> お時間があれば、少し教えて頂けたらと思います。
関連質問になるので、解決済みフラグを外しておきますね。


> 型制約をstructで指定しておいて、メソッド内で型チェックをする という理屈は理解できていると思います。
アサーションも省いた手抜き実装で良ければ、下記のように書けます。

public static class BitFlagHelper
{
 public static void SetFlag<TEnum>(ref TEnum self, TEnum flag) where TEnum : struct => self |= (dynamic)flag;
}


>>public static void SetFlag<TEnum>(ref this TEnum self, TEnum flag) where TEnum : struct
> ref this TEnum self  というように this を使っているということはこれは後述されている拡張メソッドなのでしょうか?
はい。そして拡張メソッドは、普通の静的メソッドとして使うこともできます。

先の使用例のところで
 //「propA.SetFlag(FlagEnumA.Visible);」でも可
 BitFlagHelper.SetFlag(ref propA, FlagEnumA.Visible);
という 2 つの使用例を書いていたかと思いますが、
前者が拡張メソッド構文で呼び出した場合で、
後者が静的メソッドとして呼び出した場合です。

なお、"ref" 付きの拡張メソッドを使用するには、C# 7.2 以上のバージョンが必要です。
C# のバージョンは、プロジェクトのプロパティの [ビルド] タブにある
[詳細設定] ボタンを押して、「言語バージョン」のドロップダウンから指定できます。


>> System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum);
> これは型が TEnumかチェックを行い、
TEnum かのチェックではなく
Enum かのチェックですね。
「IsEnum が true で無ければならない」という意図のアサーションです。

struct 制約だけだと、
 int n = 0;
 BitFlagHelper.SetFlag(ref n, 0);
などを許容できてしまうので、ここでは
型パラメーター TEnum が列挙型かどうかを検査しています。

Debug.Assert の代わりに、NotSupportedException や ArgumentOutOfRangeException を
throw するようにしても良いかと思います。ただ、どうせコンパイル時のチェックではなく
実行時検査にしかならないので、今回はあえて Debug.Assert を使ってみました。
この場合、リリースビルドでは検査されなくなります。



> 違っていたらメッセージボックスで呼び出し履歴を表示ですかね
.NET Core だと、既定の通知はメッセージボックスでは無いですけれどね。

アサーションを分かりやすくするために説明文を追加することも出来ます。
System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum, "型パラメーター TEnum が列挙型ではありません。:" + typeof(TEnum).FullName);


>> var bits = Convert.ToUInt64(self) | Convert.ToUInt64(flag);
> 論理和を使えるように、受け取った変数とフラグをUInt64に変換している?
そのままではビット論理和を求めることができないので、整数型に変換しています。

この方法なら、元のビットフラグが
 「public enum FlagEnumA」
 「public enum FlagEnumA : byte」
 「public enum FlagEnumA : sbyte」
 「public enum FlagEnumA : short」
 「public enum FlagEnumA : ushort」
 「public enum FlagEnumA : int」
 「public enum FlagEnumA : uint」
 「public enum FlagEnumA : long」
 「public enum FlagEnumA : ulong」
のいずれであったとしても対処できるかと。

もしくは ToUInt64 を使う代わりに、
  var bits = (dynamic)self | flag;
  self = bits;
という手抜き実装も可能です。


> この場合、bitsはUInt64として扱われるのでしょうか?
はい。「UInt64値 | UInt64値」という論理演算の結果は、UInt64 型になります。


>>self = (TEnum)System.Enum.ToObject(typeof(TEnum), bits);
> UInt64型になっている bits を TEnum型に変換し、受け取った変数に保存している?
その通りです。ここでは列挙型に戻すために Enum.ToObject を用いています。


> ref や out な引数にはプロパティを渡せないんですね。
> 勉強不足でした。
.NET Framework 4 以降で、列挙型に「HasFlag メソッド」が追加されましたが、
その逆に、フラグをセットする機能が用意されなかったのも、
このあたりに理由があるのかも知れません。


> 変数を経由して渡せば、実装は可能なのですね。
VB だとプロパティを渡せると書きましたが、これは VB のコンパイラが
こうした一時変数を自動的に生成して対応しているようです。
引用返信 編集キー/
■86942 / inTopicNo.9)  Re[4]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ MTK (41回)-(2018/04/04(Wed) 19:12:50)
No86940 (魔界の仮面弁士 さん) に返信

度々ありがとうございます。


> アサーションも省いた手抜き実装で良ければ、下記のように書けます。
>
> public static class BitFlagHelper
> {
>  public static void SetFlag<TEnum>(ref TEnum self, TEnum flag) where TEnum : struct => self |= (dynamic)flag;
> }

アサーションという言葉自体、初めてでした。
デバッグ時にだけできるのはとても便利ですね!
性善説でdynamicを使うことを想定すると確かに楽に書けますね。



> はい。そして拡張メソッドは、普通の静的メソッドとして使うこともできます。
>
> 先の使用例のところで
>  //「propA.SetFlag(FlagEnumA.Visible);」でも可
>  BitFlagHelper.SetFlag(ref propA, FlagEnumA.Visible);
> という 2 つの使用例を書いていたかと思いますが、
> 前者が拡張メソッド構文で呼び出した場合で、
> 後者が静的メソッドとして呼び出した場合です。
>
> なお、"ref" 付きの拡張メソッドを使用するには、C# 7.2 以上のバージョンが必要です。
> C# のバージョンは、プロジェクトのプロパティの [ビルド] タブにある
> [詳細設定] ボタンを押して、「言語バージョン」のドロップダウンから指定できます。

調べてみたところバージョンは C# 6.0 でした。
7.2以上になった時には使えるようになると覚えておこうと思います。
それまでは静的メソッドでやってみます。



> >> System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum);
>>これは型が TEnumかチェックを行い、
> TEnum かのチェックではなく
> Enum かのチェックですね。
> 「IsEnum が true で無ければならない」という意図のアサーションです。
>
> struct 制約だけだと、
>  int n = 0;
>  BitFlagHelper.SetFlag(ref n, 0);
> などを許容できてしまうので、ここでは
> 型パラメーター TEnum が列挙型かどうかを検査しています。
>
> Debug.Assert の代わりに、NotSupportedException や ArgumentOutOfRangeException を
> throw するようにしても良いかと思います。ただ、どうせコンパイル時のチェックではなく
> 実行時検査にしかならないので、今回はあえて Debug.Assert を使ってみました。
> この場合、リリースビルドでは検査されなくなります。
>
>>違っていたらメッセージボックスで呼び出し履歴を表示ですかね
> .NET Core だと、既定の通知はメッセージボックスでは無いですけれどね。
>
> アサーションを分かりやすくするために説明文を追加することも出来ます。
> System.Diagnostics.Debug.Assert(typeof(TEnum).IsEnum, "型パラメーター TEnum が列挙型ではありません。:" + typeof(TEnum).FullName);

なるほど、色々と勉強になります。
エラー処理についてもまだ勉強中なので、うまく使い分けられるようにしていきます。



> >> var bits = Convert.ToUInt64(self) | Convert.ToUInt64(flag);
>>論理和を使えるように、受け取った変数とフラグをUInt64に変換している?
> そのままではビット論理和を求めることができないので、整数型に変換しています。
>
> この方法なら、元のビットフラグが
>  「public enum FlagEnumA」
>  「public enum FlagEnumA : byte」
>  「public enum FlagEnumA : sbyte」
>  「public enum FlagEnumA : short」
>  「public enum FlagEnumA : ushort」
>  「public enum FlagEnumA : int」
>  「public enum FlagEnumA : uint」
>  「public enum FlagEnumA : long」
>  「public enum FlagEnumA : ulong」
> のいずれであったとしても対処できるかと。
>
> もしくは ToUInt64 を使う代わりに、
>   var bits = (dynamic)self | flag;
>   self = bits;
> という手抜き実装も可能です。

UInt64だとこんなに広い範囲に対応できるんですね。
この辺りも勉強しておかなければ・・・
dynamicは大変魅力的ですが、使い始めると私はきっと多用してしまうな とちょっと自重するようにしています^^;
いつかは使いこなしてみたいですね。



> > この場合、bitsはUInt64として扱われるのでしょうか?
> はい。「UInt64値 | UInt64値」という論理演算の結果は、UInt64 型になります。
>
>
> >>self = (TEnum)System.Enum.ToObject(typeof(TEnum), bits);
>>UInt64型になっている bits を TEnum型に変換し、受け取った変数に保存している?
> その通りです。ここでは列挙型に戻すために Enum.ToObject を用いています。
>
>
>>ref や out な引数にはプロパティを渡せないんですね。
>>勉強不足でした。
> .NET Framework 4 以降で、列挙型に「HasFlag メソッド」が追加されましたが、
> その逆に、フラグをセットする機能が用意されなかったのも、
> このあたりに理由があるのかも知れません。
>
>
>>変数を経由して渡せば、実装は可能なのですね。
> VB だとプロパティを渡せると書きましたが、これは VB のコンパイラが
> こうした一時変数を自動的に生成して対応しているようです。

書いて頂いた例についても、説明していただいて理解できたかと思います。
確かに HasFlag メソッド があるのに他のものはないですね。
伺って、なるほど・・・と思いました。

詳しく教えて頂いてありがとうございました。
解決済み
引用返信 編集キー/
■86962 / inTopicNo.10)  Re[2]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ 魔界の仮面弁士 (1603回)-(2018/04/05(Thu) 18:55:51)
No86942 (MTK さん) に返信
> 調べてみたところバージョンは C# 6.0 でした。
> 7.2以上になった時には使えるようになると覚えておこうと思います。

Visual Studio との対応表を載せておきます。

C# 2.0 … VS2005
C# 3.0 … VS2008
C# 4.0 … VS2010
C# 5.0 … VS2012
C# 6.0 … VS2015
C# 7.0 … VS2017
C# 7.1 … VS2017 15.3
C# 7.2 … VS2017 15.5
C# 7.3 … 将来のバージョン (VS2017 15.7 ?)
C# 8.0 … 将来のバージョン



■No86908 (Azulean さん) に返信
> ジェネリックではできないかもしれません。
>>// 指定されたフラグを加算するメソッド
>>public static void AddFlag(ref FlagEnumA flag, FlagEnumA state)
>>{
>>  flag = flag | state;
>>}


型制約 where で列挙型を指定することについて、
将来的に実装する方向で提案されているようです。
https://github.com/dotnet/csharplang/issues/104

実現すれば、先のコードを
  public static void SetFlag<TEnum>(ref TEnum self, TEnum flag) where TEnum : System.Enum { self |= flag; }
あるいは
  public static void SetFlag<TEnum>(ref TEnum self, TEnum flag) where TEnum : enum(int) { self |= flag; }
などと書けるようになるかも!?


ちなみに上記の C# コードは、現行バージョンのコンパイラではエラーになるのですが、
下記の IL から生成した DLL を参照設定して使う分には、VS2005 世代の C# 2.0 であっても、

    FlagEnumA bits = FlagEnumA.Enable;
    BitFlagHelper.SetFlag(ref bits, FlagEnumA.Visible);
    Console.WriteLine(bits);   //「Visible, Enable」と出力される
    int intValue = 0x2;
    // BitFlagHelper.SetFlag(ref intValue, 0x10);  // 列挙型以外はコンパイルエラー

という処理を実現することができます。


//
// 下記を「C:\TEMP\Example.IL」の名で保存して、コマンドラインから
// 「C:\Windows\Microsoft.NET\Framework\v2.0.50727\ilasm.exe /DLL /OUT=C:\TEMP\Example.dll C:\TEMP\Example.IL」
// を実行すると、「C:\TEMP\Example.dll」が生成されます。
//
.assembly Example {}
.assembly extern mscorlib { .ver 2:0:0:0 }
.class public auto ansi abstract sealed beforefieldinit BitFlagHelper extends [mscorlib]System.Object
{
  .method public hidebysig
     static void SetFlag< ([mscorlib]System.Enum) TEnum> (
       !!TEnum& self,
       !!TEnum flag
     ) cil managed 
  {
    .maxstack 8
    ldarg.0
    dup
    ldind.u4
    ldarg.1
    or
    stind.i4 
    ret
  }
}

解決済み
引用返信 編集キー/
■86976 / inTopicNo.11)  Re[3]: フラグを操作するジェネリックメソッドを作る方法
□投稿者/ MTK (42回)-(2018/04/06(Fri) 10:54:10)
No86962 (魔界の仮面弁士 さん) に返信

対応表ありがとうございます。
VSのバージョンとこんな風に対応しているんですね。

Azulean さん宛ての返信も拝見しましたが、将来の実装に期待ですね。
ありがとうございました。
解決済み
引用返信 編集キー/

このトピックをツリーで一括表示


トピック内ページ移動 / << 0 >>

このトピックに書きこむ