VB、C#、F# それぞれのインデクサ。F# コンパイラのソースを読んで。
F# コンパイラのソースを読んで
はぇ〜さん(@haxe) とtwitterにて、以下のようなやり取りがありました。
ところで #fsharp の for m in Regex.Matches(input, pattern) do () はGetEnumerator()をしてくるわりに、m : Match に解決してくれるのはなんでだろう? 仕組みがわからにあ
2013-06-20 11:03:05 via web
@haxe F# さんがベターな型をよしなに探してくれているからですね。このあたりのソースからたどって全体を見てみると「にやり」とできるかも(!) URL
2013-06-20 11:48:37 via Silverbird M to @haxe
@zecl IEnumerable<T>を実装してないのに、どうやって見つけてるのかな?って話なんです。githubは見てますよん
@haxe 貼っ付けたリンク先のtypecheckerのソースの、"リフレクションでMethInfoを見ている" から"ではなくてですか?
2013-06-20 12:02:53 via Silverbird M to @haxe
@haxe そうだったんですね。見てみてください?
2013-06-20 12:09:49 via Silverbird M to @haxe
@zecl コード読みました。ハードコーディングのグロい実装で成り立ってるんですね?。 Hashtableなんかitemの戻り値Value側がobject型だからそれに解決されるとかかなりやばいw URL
2013-06-20 13:02:28 via web to @zecl
@zecl .NET的に正しくはDefaultMemberAttributeを見るべきなんですけどね! そうすればXmlNodeListもItem()メソッドではなくItemOfプロパティがインデクサだとわかるので。
2013-06-20 13:04:39 via web to @zecl
for m in Regex.Matches(input, pattern) do () で、 MatchCollection から Match に型が解決される件は、わたしがツイートしたとおり。typecheckerのソースを見ればわかるので、まあよし。
問題はその後。
@zecl .NET的に正しくはDefaultMemberAttributeを見るべきなんですけどね! そうすればXmlNodeListもItem()メソッドではなくItemOfプロパティがインデクサだとわかるので。
2013-06-20 13:04:39 via web to @zecl
@haxe IndexerNameAttributeで別名を付けられることも考慮して...と。確かにそれも一理ありますね。でも、IndexerNameAttributeはそもそも直接利用が推奨されていない属性ですし、インデクサに別名をつけるためのモノではなく、
2013-06-20 14:53:50 via Silverbird M to @haxe
@haxe あくまで他の言語(VB と F#)のインデックス付きプロパティと同等の機能をを提供するためのものにすぎないので。そう考えると、愚直とはいえあの実装でハズしてはいないのかなぁと。
2013-06-20 14:54:13 via Silverbird M to @haxe
@zecl IndexerNameではなくDefaultMemberで。expr.[idx]ではDefaultMemberをちゃんと見てるので "abc".[1] でもCharsプロパティにアクセスできるようです。 URL
2013-06-20 15:10:30 via web to @zecl
@haxe すいませんちゃんと読みきれていませんでした; なるほどです。勝手に整理すると、そもそもインデクサを含む型にDefaultMember属性は指定できないので、C# については考える必要がなかった。XmlNodeListのような型、あるいは
2013-06-20 15:54:45 via Silverbird M to @haxe
@haxe 他の言語(VB と F#)で、Itemという名前ではないプロパティに対して任意の名前がIndexerNameで指定されていて且つ、その任意の名前がDefaultMemberに指定されている場合が考慮されてる実装になっていたんですね。くどくどと失礼しました;
2013-06-20 15:55:01 via Silverbird M to @haxe
@zecl なんか噛み合わないと思ったら。IndexerNameはソースコードのインデクサに付ける属性ですが、コンパイル時に消されます。コンパイル後のアセンブリには、IndexerNameの有無にかかわらずTypeに対してDefaultMemberが自動生成されます。
2013-06-20 16:45:31 via web to @zecl
@haxe ですです。DefaultMemberが自動生成されるときにIndexerNameで指定している場合はその名前が使われるので。それ分の考慮がなされているのだなと。
2013-06-20 16:51:01 via Silverbird M to @haxe
読み返してみると、わたしの発言が支離滅裂なところがあり。そのせいもあって、話が噛み合っていなさすぎてやばい!(はぇ〜さんごめんなさい!)。自分の中ではだいたいわかって納得したような気になっていた・・・のです(ぇ しかし、github の fsharp のソースをまじまじと見ていると、なんだか少しモヤっするものがあったので、改めて F# コンパイラのソースを舐め回すように見てみた。実際にコードも書いて確かめてみた。
すると...、いろいろ間違い(勘違い)をしていたようです(はずかしい)。C# と VBのインデクサの仕様、 C# と F# の仕様はそれぞれ異なるということは把握していたのだが、VB と F# のインデックス付きプロパティ(インデクサ)の挙動に差があることは把握できていませんでした。F# では、DefaultMemberAttribute が自動生成されないケースがあるんです。 ΩΩΩ<な、なんだってー!?
C# のインデクサ と VB のインデクサ
「C# のインデクサ」と 「VB のインデックス付きプロパティ(インデクサ)」については、岩永さんの「インデクサー(C# によるプログラミング入門) - ++C++」を参照で(丸投げ)。
C#のインデクサのサンプル:その1
public class IndexerSample1 { private int[] arr = new int[100]; public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
var sample = new IndexerSample1(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType () == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault() ; Debug.Assert(attr.MemberName == "Item");
特に細工をすることなくC#でインデクサ付きの型を作ると、"Item"というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
C#のインデクサのサンプル:その2
public class IndexerSample2 { private int[] arr = new int[100]; [System.Runtime.CompilerServices.IndexerName("SpecialItem")] public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
var sample = new IndexerSample2(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); // IndexerNameAttributeで指定された名前で生成されている Debug.Assert(attr.MemberName == "SpecialItem");
IndexerNameAttributeで指定した "SpecialItem" というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
C#のインデクサのサンプル:その3
[System.Reflection.DefaultMember("SpecialItem")] public class IndexerSample3 { private int[] arr = new int[100]; [System.Runtime.CompilerServices.IndexerName("SpecialItem")] public int this[int index] { get { if (index < 0 || index >= 100) { return 0; } else { return arr[index]; } } set { if (!(index < 0 || index >= 100)) { arr[index] = value; } } } }
インデクサ付きの型に対して DefaultMember属性を明示的に指定することはできない。コンパイラに怒られます。
MSDNライブラリ - DefaultMemberAttribute クラス
http://msdn.microsoft.com/ja-jp/library/system.reflection.defaultmemberattribute(v=vs.110).aspx
プロパティは、そのプロパティに引数が存在し、かつ、プロパティ名またはそのいずれかのアクセサーが DefaultMemberAttribute で指定された名前と一致する場合、インデクサー (Visual Basic では既定のインデックス付きプロパティ) としてインポートされます。 格納している型に DefaultMemberAttribute が存在しない場合、その型にはインデクサーは存在しません。 C# コンパイラでは、インデクサーを含むすべての型について、DefaultMemberAttribute を出力します。 C# では、既にインデクサーが宣言されている型に対し、直接 DefaultMemberAttribute で属性を指定するとエラーになります。
MSDNにもそう書いてある。
続いて、VB のインデックス付きプロパティ(インデクサ)について見ていく。
VB のインデックス付きプロパティのサンプル:その1
Public Class IndexerSample4 Private arr As Array = New Integer(100) {} Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
var sample = new IndexerSample4(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
特に細工をすることなくVBでインデクサ付きの型を作ると、C# と同様に "Item"というメンバ名で DefaultMemberAttribute が自動生成される。問題なし。
VB のインデックス付きプロパティのサンプル:その2
Public Class IndexerSample5 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Dummy(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
var sample = new IndexerSample5(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Dummy");
DefaultMember属性が暗黙的に生成されるが、IndexerName属性で指定した "SpecialItem" という名前は無視される。実際のプロパティ名(DisplayName)である"Dummy" で作られる。これは知ってた。
VB のインデックス付きプロパティのサンプル:その3
<System.Reflection.DefaultMember("Hoge")> _ Public Class IndexerSample6 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
DefaultMember属性に、インデクサのプロパティ名と異なるメンバ名が指定されると競合が発生する。こんなの初めて書いたw
VB のインデックス付きプロパティのサンプル:その4
<System.Reflection.DefaultMember("Item")> _ Public Class IndexerSample7 Private arr As Array = New Integer(100) {} <System.Runtime.CompilerServices.IndexerName("SpecialItem")> _ Default Public Property Item(ByVal index As Integer) As String Get If index < 0 OrElse index >= 100 Then Return 0 Else Return arr(index) End If End Get Set(ByVal Value As String) If Not (index < 0 OrElse index >= 100) Then arr(index) = Value End If End Set End Property End Class
DefaultMember属性に、インデクサのプロパティ名と同様のメンバ名を指定すると問題ない。C# とは異なり、VB ではインデクサ付きの型でDefaultMember属性を明示的に指定することが可能。
var sample = new IndexerSample7(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが指定されているので、当然存在する Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
DefaultMemberAttributeが明示的に指定されているので、当然存在する。メンバ名もそのまんま。ここまでは問題ないです。
F# のインデックス付きプロパティ(インデクサ)
F#のインデックス付きプロパティのサンプル:その1
type IndexerSample8 () = let arr : int [] = Array.zeroCreate 100 member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample8(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
F#のインデックス付きプロパティのサンプル:その2
type IndexerSample9 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample9(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
C#と異なり、VBと同じ挙動。知ってた。そりゃそーですよね。
F#のインデックス付きプロパティのサンプル:その3
[<System.Reflection.DefaultMember("Item")>] type IndexerSample10 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample10(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
VBと同じように、明示的にDefaultMember属性を指定することができる。
F#のインデックス付きプロパティのサンプル:その4
[<System.Reflection.DefaultMember("Hoge")>] type IndexerSample11 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Item with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
VBと同じように、競合が発生してエラーとなるかと思いきや・・・、コンパイルが通る!!!
var sample = new IndexerSample11(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr.MemberName == "Item");
DefaultMember属性のメンバ名に誤った名称が指定されている場合、それは無視される。実プロパティ名をメンバ名として DefaultMember属性 が自動生成される。これは予想外の動き!!!
しかし、この場合
let sample = new IndexerSample11() let v = sample.[0]
なぜか、DefaultMember属性のメンバ名が"Hoge"だって言われる。なので、インデクサにアクセスできない。ど、どういうことだってばよ!?
確認のためのソースが悪かったorz
var sample = new IndexerSample11(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attrs2 = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>(); Debug.Assert(attrs2.Any( x => x.MemberName == "Hoge"));
DefaultMember属性が暗黙的に生成されているんだけど、明示的に指定したやつとだぶっちゃっている。あらまあ。
ってかこれ、バグっぽいちゃーバグっぽいゼ!?
F#のインデックス付きプロパティのサンプル:その5
type IndexerSample12 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("SpecialItem")>] member this.Dummy with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample12(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成されない Debug.Assert(!attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attr = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>().FirstOrDefault(); Debug.Assert(attr == null);
IndexerSample5のように、VBと同じ挙動をするかと思いきや、なんと、DefaultMemberAttributeが自動生成されないケースがここで発生。これは、F#のインデクサ付き型で、DefaultMemberAttributeが自動生成されないケースが存在すると言うよりも、「F# は、VB とは挙動が異なり、任意の名称のプロパティではでインデクサ付きの型とはならない」と言うのが正しいだろう。なんということでしょう。F# コンパイラによって「DefaultMemberAttributeが自動生成されない」ケースがあった!!!これは知らなかった。意図的なのかどうなのかわからないが、C#、VB いずれとも異なる挙動になるように作られている。
ってかこれ、バグっぽいちゃーバグっぽいゼ!?
F# でインデクサにアクセスすることができない。当然、こうなります。
F# コンパイラのソースを見てみようのコーナー
では、コンパイラの中で一体何が起こっているんでしょう。ソースを見てみる。
IL的に DefaultMemberAttribute を自動生成しているらしいこのあたりを引用する。
https://github.com/fsharp/fsharp/blob/master/src/fsharp/ilxgen.fs#L6239
let defaultMemberAttrs = // REVIEW: this should be based off tcaug_adhoc_list, which is in declaration order tycon.MembersOfFSharpTyconSorted |> List.tryPick (fun vref -> let name = vref.DisplayName match vref.MemberInfo with | None -> None | Some memberInfo -> match name, memberInfo.MemberFlags.MemberKind with | ("Item" | "op_IndexedLookup"), (MemberKind.PropertyGet | MemberKind.PropertySet) when nonNil (ArgInfosOfPropertyVal cenv.g vref.Deref) -> Some( mkILCustomAttribute cenv.g.ilg (mkILTyRef (cenv.g.ilg.mscorlibScopeRef,"System.Reflection.DefaultMemberAttribute"),[cenv.g.ilg.typ_String],[ILAttribElem.String(Some(name))],[]) ) | _ -> None) |> Option.toList
「// REVIEW: this should be based off tcaug_adhoc_list, which is in declaration order」のコメントも気になっちゃいますが、それは置いていおいて・・・。ここを起点に全体のソースを舐め回すように眺めてみる。ふむふむなるほど。F# コンパイラさん、IndexerName 属性はまったく見ていないご様子。そして、「let name = vref.DisplayName」を見ればわかるように、実プロパティ名を参照している。そして、実プロパティ名が、"Item" 、"op_IndexedLookup"のいずれかの場合に限り、実プロパティ名を使って DefaultMemberAttribute が暗黙的に生成されていることがわかります。
では、DisplayNameが "op_IndexedLookup"であるとき、とはどんな時か。次のサンプルのようなケースのときである。
F#のインデックス付きプロパティのサンプル:その6
[<System.Reflection.DefaultMember("Hoge")>] type IndexerSample13 () = let arr : int [] = Array.zeroCreate 100 [<System.Runtime.CompilerServices.IndexerName("Fuga")>] member this.Hoge with get(index) = if index < 0 || index >= 100 then 0 else arr.[index] and set index value = if not (index < 0 || index >= 100) then arr.[index] <- value
var sample = new IndexerSample13(); var attrs = System.Attribute.GetCustomAttributes(sample.GetType()); // DefaultMemberAttributeが自動生成される Debug.Assert(attrs.Any(x => x.GetType() == typeof(DefaultMemberAttribute))); var attrs2 = attrs.Where(x => x.GetType() == typeof(DefaultMemberAttribute)).Cast<DefaultMemberAttribute>(); Debug.Assert(attrs2.Any( x => x.MemberName == "Hoge"));
DefaultMemberAttributeはダブっていない。もちろんIndexerName属性も華麗にスルーです。
当然、インデクサにF#からアクセスできる。
自分のためのまとめ
・F# コンパイラは、DefaultMember属性を暗黙的に生成するとき、C#とは異なり、IndexerName属性は華麗にスルーされる。VBと同じ。
・F#のインデックス付きプロパティと、VBのインデックス付きプロパティは違う。思い込みイクナイ。
・F# コンパイラは、インデックス付きプロパティがあっても DefaultMember 属性を暗黙的に生成しない場合がある(てゆーか、それインデクサ付きの型じゃないですしおすし)。
・VB、C#、F# は、それぞれインデクサの仕様が異なるので気をつけましょう。
・ってかこれ、バグっぽいちゃーバグっぽいゼ!?(DefaultMember属性のダブりとかマジやべぇ)*1
気まぐれでサラッとだけ書くつもりだったのに。なんやかんやで無駄に長くなって疲れた(内容しょぼいのに!)。
お疲れ様でした。
*1:まぁ、ふつーにインデクサを使うだけなら問題にならないので、「仕様です」っちゃー仕様ですね
勇気を出して GO! GO! GO FOR IT !! 明日 キミにメールしようかな
西野カナ / GO FOR IT!!
幸せすぎると 恐くなるの コップの水が溢れだすように
HOME MADE 家族 - Love is... feat. Ms.OOJA (Short ver.)