読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

VB、C#、F# それぞれのインデクサ。F# コンパイラのソースを読んで。

プログラミング F#3.0 F# C# VB.NET


F# コンパイラのソースを読んで


はぇ〜さん(@haxe) とtwitterにて、以下のようなやり取りがありました。

for m in Regex.Matches(input, pattern) do () で、 MatchCollection から Match に型が解決される件は、わたしがツイートしたとおり。typecheckerのソースを見ればわかるので、まあよし。



問題はその後。


読み返してみると、わたしの発言が支離滅裂なところがあり。そのせいもあって、話が噛み合っていなさすぎてやばい!(はぇ〜さんごめんなさい!)。自分の中ではだいたいわかって納得したような気になっていた・・・のです(ぇ しかし、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");


C#VBと同じ。問題なし。




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 属性を暗黙的に生成しない場合がある(てゆーか、それインデクサ付きの型じゃないですしおすし)。
VBC#、F# は、それぞれインデクサの仕様が異なるので気をつけましょう。
・ってかこれ、バグっぽいちゃーバグっぽいゼ!?(DefaultMember属性のダブりとかマジやべぇ)*1



気まぐれでサラッとだけ書くつもりだったのに。なんやかんやで無駄に長くなって疲れた(内容しょぼいのに!)。



お疲れ様でした。

*1:まぁ、ふつーにインデクサを使うだけなら問題にならないので、「仕様です」っちゃー仕様ですね