WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

List(Of T) と Count プロパティ

by WebSurfer 12. December 2015 15:09

VB.NET の場合、定義されているプロパティが同じクラスで定義されている(もしくは継承したクラスで定義されている)同名のメソッドを隠してしまうという話を、List(Of T) と Count プロパティを例にとって説明します。

例として以下のコードを考えます。

Dim e As IEnumerable(Of Int32) = Enumerable.Range(1, 10)

' これは OK。拡張メソッド Count を呼び出す
Console.WriteLine(e.Count(Function(n) n >= 5))

Dim x As List(Of Int32) = e.ToList

' ビルドエラー「Public ReadOnly Property Count As Integer' には引
' 数がないため、戻り値の型をインデックス化できません。」になる
Console.WriteLine(x.Count(Function(n) n >= 5))

'明示的に拡張メソッドを呼び出せば OK
Console.WriteLine(Enumerable.Count(x, Function(n) n >= 5))

e は IEnumerable(Of T) 型で Count という名前では拡張メソッドの Count のみを実装しています。(同名のプロティは実装されていません)

なので、e.Count(Function(n) n >= 5) で拡張メソッドの Count を呼び出せています。

一方、x は IEnumerable(Of T) から ToList メソッド を使って作成した List(Of T) 型になります。List(Of T) 型には Count プロパティCount 拡張メソッド(Enumerable.Count のオーバーロード)の両方が実装されています。

VB.NET の場合、List(Of T) 型に実装されている Count プロパティによって拡張メソッドの Count が隠されてしまい、コンパイラは Count プロパティが呼ばれていると判断して上のソースにコメントしたようなビルドエラーになります。

これは VB.NET のコンパイラの問題で、同名のプロパティとメソッドを区別できないところからきているようです。

そのことを書いた Micosoft の公式文書は見つかりませんでしたが、MSDN フォーラムの記事で、Matt Warren - MSFT さんが、

"VB compiler is matching the lists Count property instead of the Enumerable.Count() extension method and the Count property is blocking the visibility of the extension method's other signature."

Timothy Ng MSFT さんが、

"in VB, properties shadow methods (and thus, extension methods) by name."

と書いた説明で自分的には納得しています。

そのことは List(Of T) と Count プロパティ / メソッドに限った話ではなく、以下のような自作のクラスで定義した同名のプロパティとメソッドでも再現できます。

Module Module1
    Sub Main()
        Dim sample As SampleClass = New SampleClass()

        ' SampleClass の Name プロパティが呼ばれる
        Console.WriteLine(sample.Name)

        ' 同じく SampleClass の Name プロパティが呼ばれる。
        ' VB.NET の ( ) は、C# の [ ] と同様に、配列の要素にアク
        ' セスするので、sample.Name(1) は "Property called." の 
        ' 2 文字目の 'r' となる。
        Console.WriteLine(sample.Name(1))

        ' 同じく SampleClass の Name プロパティが呼ばれる。
        ' ( ) は文字列(配列)"Property called." の要素にアクセ
        ' スする。引数 "abc" は Int32 型に変換できないのでビルド
        ' エラーになる。
        Console.WriteLine(sample.Name("abc"))
    End Sub
End Module

Public Class SampleClass
    Inherits SampleBaseClass
    ' Shadows キーワードを付けないと以下の警告が出るが、付けな
    ' くても結局は shadow される。
    ' 「property 'Name' は、function 'Name' とベース class 
    ' 'SampleBaseClass' で競合しています。'Shadows' として宣言
    ' されなければなりません。}
    Public ReadOnly Property Name() As String
        Get
            Return "Property called."
        End Get
    End Property
End Class

Public Class SampleBaseClass
    Public Function Name(ByVal i As Int32) As String
        Return "Instanse Method called."
    End Function
End Class

Public Module MyExtension
    <System.Runtime.CompilerServices.Extension()>
    Public Function Name(ByVal x As SampleBaseClass, _ 
                         ByVal s As String) As String
        Return "Extension Method called."
    End Function
End Module

なお、C# の場合はこのような問題なく、以下のコードでいずれも期待した通りの結果 6 を取得できます。

IEnumerable<int> e = Enumerable.Range(1, 10);

Console.WriteLine(e.Count(n => n >= 5));

List<int> x = e.ToList();

Console.WriteLine(x.Count(n => n >= 5));
Console.WriteLine(Enumerable.Count(x, n => n >= 5));

Tags: ,

.NET Framework

匿名型と Distinct メソッド

by WebSurfer 8. December 2015 19:44

IEnumerable<T> で T が匿名型の場合は Distinct() メソッドで期待通り重複のない結果が得られますが、T にカスタムデータ型を使った場合は、

  1. そのカスタムデータ型に IEquatable<T> インターフェイスを継承させて Equals メソッドを実装し、GetHashCode メソッドを override する(具体例は MSDN ライブラリ Enumerable.Distinct<TSource> メソッド (IEnumerable<TSource>) のサンプルコード参照)、または、
  2. IEqualityComparer<T> を継承したクラスを作成し、それを引数にとって値を比較できるバージョンの Distinct メソッドを使う(具体例は MSDN ライブラリ Enumerable.Distinct<TSource> メソッド (IEnumerable<TSource>, IEqualityComparer<TSource>) のサンプルコード参照)。

のいずれかの方法を取る必要があるそうです。知ってました? 実は自分は知らなかったです。匿名型を使っている限りは問題なかったので。(汗)

ちなみに、上に紹介した MSDN ライブラリのサンプルコードで、Product 型に替えて匿名型を使えば以下のように Distinct() メソッドを使って重複しない結果を取得できます。

var products = new[] { 
    new { Name = "apple", Code = 9 }, 
    new { Name = "orange", Code = 4 }, 
    new { Name = "apple", Code = 9 }, 
    new { Name = "lemon", Code = 12 } };

var noduplicates = products.Distinct();

foreach (var product in noduplicates)
    Console.WriteLine(product.Name + " " + product.Code);

/* 結果は:
apple 9
orange 4
lemon 12
*/

ただ、それで何故 Distinct() が使えるのかがズバリ書いてある Microsoft の公式文書が見つからないという不安はありますが。

一応、匿名型における Equals, GetHashCode, ToString メソッドおよび IEquatable<T> インターフェイスの実装について以下の文書があるのは見つけました。

上のコード new { Name = "apple", Code = 9 } で作られる匿名型にも上記が当てはまるのであろうとは思っていますが・・・

(注)VB.NET には Key というキーワードがあってそれを付与して定義したプロパティのみが比較の対象となるようです。一方、C# は上の記事にもあるように全てのプロパティが比較の対象になります。

Tags: ,

.NET Framework

正規表現パターンにコメント

by WebSurfer 4. July 2015 17:33

.NET Framework の C# のコードで、正規表現のパターンにコメントをつける方法を備忘録として書いておきます。

正規表現のパターンにコメント付与

上の画像を見れば一目瞭然で、それ以上の説明は不要かもしれませんが、それではブログの記事としてはちょっと寂しいので追加情報なども以下に書いておきます。

正規表現については、自分的には MSDN の記事 ASP.NET の正規表現 が一番分かりやすく、いつも参考にさせてもらっています。

おかげさまで、自分でもある程度パターンを作れるようにはなったのですが、自分で作っておきながら後になって読むと意味不明ということがあります。

そういう時のために、正規表現パターンにコメントをつけておくと良いという MDSN の記事を見つけて真似しています。

その記事見つからなくなってしまったので、以前自分が書いたコードを探して、それを参考にしてコメントをつけていましたが、探して見つけるのが結構大変ということで、ブログに書いておくことにしました。

以下は完全に余談ですが・・・

例として、パスワードの文字列で、条件として「半角大文字アルファベットと半角数字のみの 4 文字以上、8 文字以下で構成され、それぞれ最低 1 文字を含む」というケースを考えてみます。

そのようなケースでは、上に紹介した「ASP.NET の正規表現」のページの「高度なトピック」のセクションに書いてある "ルックアラウンド処理" を利用するのが便利だと思います。

「正の先読み」および「負の先読み」の両方のケースで書いてみました。前者が下のコードの regex1、後者が regex2 です。

Regex regex1 = new Regex(@"
    ^               # 開始のアンカー
    (?=.*\d)        # 数字が最低 1 文字あること
    (?=.*[A-Z])     # 英大文字が最低 1 文字あること
    [A-Z0-9]{4,8}   # 英大文字または数字が 4 ~ 8 文字
    $               # 終了のアンカー",
    RegexOptions.IgnorePatternWhitespace);

Regex regex2 = new Regex(@"
    (?!^[0-9]*$)    # 全部が数字ということはない
    (?!^[A-Z]*$)    # 全部が英大文字ということはない
    ^               # 開始のアンカー
    ([A-Z0-9]{4,8}) # 英大文字または数字が 4 ~ 8 文字
    $               # 終了のアンカー",
   RegexOptions.IgnorePatternWhitespace);

string[] testStrings = { "AB1", "AB12", "ABCDEF", "123456", 
                         "ABCDEFG1", "1234567A", "ABCD12345", 
                         "ABC123dE", "ABCあ123", "ABC%1234" };

foreach (string s in testStrings)
{
    Console.WriteLine(s + " => " + regex1.IsMatch(s) + " (1)");
    Console.WriteLine(s + " => " + regex2.IsMatch(s) + " (2)");
}

// 結果は:
// AB1 => False (1)
// AB1 => False (2)
// AB12 => True (1)
// AB12 => True (2)
// ABCDEF => False (1)
// ABCDEF => False (2)
// 123456 => False (1)
// 123456 => False (2)
// ABCDEFG1 => True (1)
// ABCDEFG1 => True (2)
// 1234567A => True (1)
// 1234567A => True (2)
// ABCD12345 => False (1)
// ABCD12345 => False (2)
// ABC123dE => False (1)
// ABC123dE => False (2)
// ABCあ123 => False (1)
// ABCあ123 => False (2)
// ABC%1234 => False (1)
// ABC%1234 => False (2)

「正の先読み」というのが (?=<pattern>) という形のもので、対象文字列を先読みしていって <pattern> の条件に合えば true になります。例えば、上のコードでいうと regex1 の (?=.*\d) が該当します。<pattern> は .*\d で、「任意の文字 0 回以上の繰り返しのあと数字がある」という条件になります。

「負の先読み」というのは (?!<pattern>) という形のもので、「正の先読み」の逆すなわち否定になり、<pattern> の条件に合わないものが true になります。上のコードの regex2 の (?!^[0-9]*$) の場合、<pattern> は ^[0-9]*$ で、「文字列の最初から最後まで全ての文字が数字ではない」という条件になります。

後者の方はホントにこれでいいのか自信がないですが(汗)、上のコードの中のコメントに書いたマッチするか否かの結果を見る限りでは、よさそうな感じです。(笑)

Tags:

.NET Framework

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  December 2019  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar