C# インターフェース - IComparable, IComparable(T)

C# では覚えておくと役に立つインターフェースがいろいろとあります。 このうちの幾つかについて紹介していきたいと思います。
インターフェース 対象 使えるようになる機能
IComparable, IComparable<T> コンテナーに格納するクラス 格納したコンテナーのソート、検索など
IEnumerable コンテナークラス foreach
IEnumerable<T> LINQ の各種メソッド
IDisposable ファイルなどの後処理のタイミング管理が必要なクラス using

今回は IComparable, IComparable<T> インターフェースについての説明です。

用途

配列(Array)などのコンテナークラスでは数値を格納した場合に、 ソートや二分探索(ソート済みのデータから高速に検索)などができます。

IComparable または IComparable<T> インターフェースを継承しておけば、自作のクラスでもこれらの機能が使えるようになります。
また、 SortedList のような比較を必要とするコンテナーでは継承していないと格納することができません。

コンテナーに格納するようなクラスでは実装しておいた方がいいでしょう。

IComparable

まず IComparable から説明します。

実装するメソッド

IComparable インターフェースを継承した場合、CompareTo() メソッドを実装する必要があります。
int CompareTo(Object obj);
これは C 言語で言えば、文字列の strcmp() に相当する比較用メソッドです。
比較対象(obj)を受け取り、自身(this)と比べた結果を数値として返します。
比較 戻り値
obj < this -1 以下の整数
obj == this 0
this < obj 1 以上の整数

cs_icomparable.png

サンプル

サンプルとして点クラスを作成しました。 コンパイルする場合は以下のコマンドを実行します。
 > csc IComparableSample.cs Point_IComparable.cs
csc.exe の使用したコンパイル方法については以前の記事を見て下さい。
点クラス Point は x, y のプロパティを持たせています。
CompareTo() メソッドはまず x で比較し、 同じ場合に y で比較するという仕様にしました。

Point_IComparable.cs(抜粋) :
class Point : IComparable
{
    public int x  { get; set; }
    public int y  { get; set; }

    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    /// <summary>
    ///   比較メソッド
    /// </summary>
    public int CompareTo(Object obj)
    {
        if (obj == null) return 1;

        Point other = (Point)obj;
        if (other.x == x) {
            return y - other.y;
        }       
        return x - other.x;
    }
}
作成した Point クラスを使った例は IComparableSample.cs に記述しています。
ここで、BinarySearch() の例を後にしているのは、ソートしたデータでなければ検索できないためです。

IComparableSample.cs(抜粋) :
class Program
{
    static void Main()
    {
        // Point を格納したデータを準備
        Point [] ary = {
            new Point(1, 2),
            new Point(3, 5),
            new Point(1, 9),
            new Point(4, 1),
            new Point(3, 1)
        };          
        DumpArray("Original", ary);
            
        // データのソート
        Array.Sort(ary);
        DumpArray("Sorted", ary);

        // 二分探索
        Console.WriteLine("## Binary Search ##");
        var target = new Point(3, 1);
        int pos = Array.BinarySearch(ary, target);
        Console.WriteLine("{0} @ {1}", target, pos);
    }
}
実行結果 :
## Original ##
(1, 2) (3, 5) (1, 9) (4, 1) (3, 1) 

## Sorted ##
(1, 2) (1, 9) (3, 1) (3, 5) (4, 1) 

## Binary Search ##
(3, 1) @ 2

IComparable<T>

IComparable<T> は IComparable のジェネリック版です。 どちらを使っても同じように使うことができます。

わかりやすいように先に IComparable の説明を行いましたが、 実際に使うのは ジェネリック版の IComparable<T> が良いでしょう。
CompareTo() 実装時にキャストしなくて済みますし、若干処理が速いようです。 IComparable<T> の方がいいなら、なぜ IComparable があるのかというと、 IComparable が先にできていたためです。 ジェネリックは C# 2.0 から追加されています。

実装するメソッド

IComparable<T> の場合はジェネリックな CompareTo() を実装します。
int CompareTo(T other);
cs_icomparable_t.png

サンプル

点クラスを使用する側のコードは IComparable と同じ IComparableSample.cs を使用します。
 > csc IComparableSample.cs Point_IComparable_T.cs

Point_IComparable_T.cs(抜粋) :
class Point : IComparable<Point>
{
    public int CompareTo(Point other)
    {
        if (other == null) return 1;
             
        if (other.x == x) {
            return y - other.y;
        }       
        return x - other.x;
    }   
}
実行結果も同じです。

ジェネリックな Point

ついでに Point をジェネリックにしたサンプルも紹介します。

前のサンプルは Point の x, y の型は int 固定でしたが、 double などの任意の型を仕様できるようになります。
ただし、 CompareTo() で比較する必要があるので、 完全に任意ではなく、x, y の型は IComparable を継承しているなければなりません。

クラスのジェネリックと IComparable がジェネリックかどうかは関係ありません。 サンプルでは IComparable<T> で実装していますが、 IComparable でも同じように実装できます。


サンプルは今度は 1 つのファイルに記述しています。
 > csc GenericPointSample.cs 

GenericPointSample.cs(抜粋) :
class Point<Type> : IComparable<Point<Type> >
    where Type : IComparable
{
    public Type x  { get; set; }
    public Type y  { get; set; }

    public Point(Type x, Type y)
    {
        this.x = x;
        this.y = y;
    }

    public int CompareTo(Point<Type> other)
    {
        if (other == null) return 1;
            
        if (x.CompareTo(other.x) == 0) {
            return y.CompareTo(other.y);
        }       
        return x.CompareTo(other.x);
    }

} // Point

実行結果は他のサンプルと同じです。


スポンサーサイト



 

C# インターフェース - IEnumerable と yield

今回は C# のインターフェースのうち、 IEnumerable インターフェースについての説明です。
また、 yield の使用方法も説明をあわせて行なっています。

用途

IEnumerable は自作のコンテナークラス などで継承します。
IEnumerable を継承していると foreach で要素にアクセスすることができるようになります。

foreach を使えるようになるだけでは、大したメリットではないかも知れません。 実際には IEnumerable を継承するのであれば、ジェネリック版の IEnumerable<T> を継承した方がいいでしょう。
ただし、わかりやすさのために先に IEnumerable を説明しています。 IEnumerable<T> については次回説明したいと思います。

実装するメソッド

IEnumerable インターフェースを継承した場合、GetEnumerator() メソッドを実装する必要があります。
IEnumerator GetEnumerator()
cs_ienumurable.png
戻り値の IEnumerator もインターフェースで、 GetEnumerator() を実装するにはさらに IEnumerator を継承したクラスを用意しておく必要があります。しかし、この IEnumerator を実装したクラスを用意するのは結構面倒です。
これを簡単にする yield という機能があります。 この yield を使う場合、使わない場合の 2 通りの方法を説明します。

IEnumerator の使用

まずは yield を使わずに IEnumerator の継承クラスを使う実装について紹介します。
ただし、ちゃんと実装するのは面倒なので、 サンプルでは既存のものを使って少し楽をすることにします。

このサンプルとして前回作成した 点クラス(Point) を複数持つ 多角形クラス(Polygon) を作成しました。 コンパイル:
 > csc IEnumerableSample.cs Point.cs Polygon_Enumerator.cs
多角形クラスをただ実装するのであれば、 ArrayList などのクラスを継承した方が簡単です。
しかし、まじめに作成しようとした場合、図形の抽象クラスから継承するといったことになるかと思います。 その際には IEnumerable と抽象クラスを多重継承します。
サンプルはそういった場合と考えて下さい。

GetEnumerator() の実装には点を格納している Array メンバーの GetEnumerator() を利用しています。

Polygon_Enumerator.cs (抜粋) :
class Polygon : IEnumerable
{
    private Point[] _points;

    public Polygon(Point[] points)
    {
        if (points != null)
        {
            _points = new Point[points.Length];
            for (int cnt = 0; cnt < points.Length ; cnt++)
            {
                _points[cnt] = new Point(points[cnt]);
            }   
        }       
    }

    public IEnumerator GetEnumerator()
    {
        return _points.GetEnumerator();
    }
}
foreach を使ったアクセス例は IEnumerableSample.cs に記述しています。

IEnumerableSample.cs (抜粋) :
class Program
{
    static void Main()
    {
        Point[] points = {
            new Point(0, 0),
            new Point(5, 0),
            new Point(0, 5)
        };
        Polygon poly = new Polygon(points);
            
        Console.Write("Polygon = {\n  ");
        foreach (Point pos in poly)
        {
            Console.Write("{0} ", pos);
        }
        Console.WriteLine("\n}");
    }
}
実行結果 :
Polygon = {
  (0, 0) (5, 0) (0, 5) 
}

yield の使用

前のサンプルではメンバーの GetEnumerator() を委譲して使いましたが、 実際に自作のコンテナー等で IEnumerator を継承したクラスから作成するのは面倒です。

Ruby では似たようなことを行うとき、 yield を使った内部イテレーターの仕組みがあるので、簡単に作成することができます。 Ruby の経験があると C# のものは非常に面倒くさいと感じてしまいます。
しかし、 C# にも yield の機能が後から追加されました。 C# でも簡単に作成できるようになっています。(実際には再帰が書きづらいなど、完全に同じとも言えないのですが...)

今度は yield を使った方法を紹介します。
先ほどのサンプルを yield を使って書き直してみます。 Polygon_yield.cs 以外は同じファイルを使用しています。 コンパイル:
 > csc IEnumerableSample.cs Point.cs Polygon_yield.cs
yield return を使って、返したい要素を返します。
戻り値が IEnumerator になっていませんが、その辺は C# のコンパイラーが補ってくれます。

Polygon_yield.cs (抜粋) :
public IEnumerator GetEnumerator()
{
    foreach (Point pos in _points)
    {
        yield return pos;
    }
}
実行結果は前のサンプルと同じです。

なお、条件で返す作業を中断したい場合には yield break を使用します。

抽象図形クラスを継承したサンプル

多角形クラスのサンプルだとあまり yield のメリットが感じられないかもしれないので、 もう一つサンプルを紹介します。

もう少しちゃんと作って図形の抽象クラスから四角形と多角形を継承することにします。
cs_shape.png
コンパイル:
 > csc Shape.cs Point.cs
四角形で GetEnumerator() を実装する場合には、 yield で 4 回、点を返しています。
このようにループを回すような処理でなくても yield を使うことができます。

Shape.cs(抜粋) :
class Rectangle : Shape
{

    override public IEnumerator GetEnumerator()
    {
        yield return new Point(x,   y);
        yield return new Point(x+w, y);
        yield return new Point(x+w, y+w);
        yield return new Point(x,   y+w);
    }
}
使用方法のサンプルでは、四角形、多角形オブジェクトを Shape の配列として格納し、 ともに foreach でアクセスしています。

Shape.cs(抜粋) :
static void Main()
{
    Shape[] shapes = {
        new Rectangle(10, 5, 10, 5),
        new Polygon(new Point[] {
                new Point(0, 0),
                new Point(5, 0),
                new Point(0, 5)
            })
    };
            
    foreach (Shape fig in shapes)
    {
        Console.Write("{0} = [ ", fig);
        foreach (Point pos in fig)
        {
            Console.Write("{0} ", pos);
        }
        Console.WriteLine("]");     
    }
}
実行結果 :
Geometry.Rectangle = [ (10, 5) (20, 5) (20, 15) (10, 15) ]
Geometry.Polygon = [ (0, 0) (5, 0) (0, 5) ]

yield ブロック

yield の利用方法をもう一つ紹介します。
先程は自作のクラスに対して yield を使って IEnumerable の実装を行いましたが、 今度は IEnumerable を実装したコンテナーを戻り値として返す関数を yield で作成します。


サンプルは多角形から頂点以外の点の集合を返す関数にします。

折れ線(polyline)には始点と終点が一致した閉じた折れ線(close polyline) と一致しない開いた折れ線(open polyline)があります。
cs_close_polyline.png
多角形クラスの頂点を順にアクセスした場合には、開いた折れ線となります。
しかし、計算の時などで閉じた折れ線が欲しい時があります。 この 各頂点 + 始点 を返す関数を作成します。

コンパイル:
 > csc YieldBlockSample.cs Point.cs
戻り値の型と最後にもう一度始点を返しているところを除いて、実装はほぼ GetEnumerator() と同じです。

YieldBlockSample.cs (抜粋) :
class Polygon : IEnumerable
{
    
    public IEnumerator GetEnumerator()
    {
        foreach (Point pos in _points)
        {
            yield return pos;
        }
    }

    /// <summary>
    ///   閉じた折れ線の取得
    /// </summary>
    public IEnumerable ClosedPolyline()
    {
        foreach (Point pos in _points)
        {
            yield return pos;
        }
        yield return _points[0];
    }   
}
戻り値の使用例と実行結果です。
static void Main()
{
    Point[] points = {
        new Point(0, 0),
        new Point(5, 0),
        new Point(0, 5)
    };
    Polygon poly = new Polygon(points);
            
    Console.Write("Polygon = {\n  ");
    foreach (Point pos in poly)
    {
        Console.Write("{0} ", pos);
    }
    Console.WriteLine("\n}");

            
    Console.Write("Closed Polyline = {\n  ");
    foreach (Point pos in poly.ClosedPolyline())
    {
        Console.Write("{0} ", pos);
    }
    Console.WriteLine("\n}");
}
実行結果 :
Polygon = {
  (0, 0) (5, 0) (0, 5) 
}
Closed Polyline = {
  (0, 0) (5, 0) (0, 5) (0, 0) 
}

遅延評価

"一旦コンテナーを作って貯めるのは、点が多い場合にはメモリーがもったいない"と思われた方もいるかも知れません。
しかし、 C# では遅延評価という機能があるため、 無駄にメモリーを確保したりはしていません。

これを確認するためにサンプルを少し修正します。 コンパイル:
 > csc YieldBlockSample_lazy.cs Point.cs
今度は閉じた折れ線を返すときに要素の参照をそのまま返すのではなく、 新しいオブジェクトを作って返すようにしています。

YieldBlockSample_lazy.cs (抜粋) :
public IEnumerable ClosedPolyline()
{
    foreach (Point pos in _points)
    {
        yield return new Point(pos);
    }
    yield return new Point(_points[0]);
}
使用時の処理を次の手順に変更しています。
  1. ClosedPolyline() の戻り値を変数に格納
  2. 元の多角形の要素に変更を加える
  3. 戻り値のコンテナーに対して foreach でアクセス
static void Main()
{           

    // 遅延処理の確認
    var polyline = poly.ClosedPolyline();
    Console.WriteLine("polyline = {0}", polyline);
    poly[1].x = 2;
    Console.WriteLine("Change poly[1].x = {0}", poly[1].x);

    Console.Write("Closed Polyline = {\n  ");
    foreach (Point pos in polyline)
    {
        Console.Write("{0} ", pos);
    }
    Console.WriteLine("\n}");
}
実行結果 :
Polygon = {
  (0, 0) (5, 0) (0, 5) 
}
polyline = IEnumerableSample.Polygon+<ClosedPolyline>d__6
Change poly[1].x = 2
Closed Polyline = {
  (0, 0) (2, 0) (0, 5) (0, 0) 
}
実行結果では ClosedPolyline() の戻り値のクラス名を表示しています。 C# が自動的に作成したものなので、よくわからない名前になっていますが、 実はこれは単なるコンテナーではありません。

実行結果を見ると ClosedPolyline() を実行し、 戻り値をとった後に値を変えているにもかかわらず、 戻り値のコンテナーにまで影響をあたえています。
yield で返すときに新しくオブジェクトを作っているので、 参照だからというわけでもありません。

すなわち、 foreach の度に ClosedPolyline() の内部処理を行なっている ということです。
このようにすぐに関数内の処理を行わず、使用される時に処理が実行されることを遅延評価と言います。

ただし、一旦コンテナーの内容を確定させたいこともあります。 この場合には ToArray() などのメソッドで別のコンテナーに変換することによって処理を行なっておくこともできます。


 

C# インターフェース - IEnumerable(T)

今回は C# のインターフェースの IEnumerable<T> に関する説明です。
このインターフェースは非常に便利なのですが、 実装するにはちょっとしたテクニックが必要となります。 知らないとなかなか思いつきにくいところなので、 覚えておいて損はないと思います。

用途

前回紹介したIEnumerable インターフェース と同様に IEnumerable<T> は自作のコンテナークラス などで継承します。

IEnumerable を継承したクラスは foreach で使えるようになります。
IEnumerable<T> の場合は、 foreach で使えることに加えて、より大きなメリットがあります。

それは System.Linq using しておくことによって、 LINQ で定義された多くの拡張メソッドが使用可能になることです。 これは Ruby で言えば、 Enumerable を Mix-in したようなものです。
自前のコンテナークラスを用意した場合には、 IEnumerable<T> を継承しておくのがお勧めです。

実装するメソッド

IEnumerable<T> を継承するには GetEnumerator()メソッドを実装します。
IEnumerator<T> GetEnumerator();
加えて、 IEnumerator の GetEnumerator() メソッドも実装する必要があります。
IEnumerator GetEnumerator()
これは IEnumerator<T> が単に IEnumerator のジェネリック版というだけでなく、 IEnumerator を継承しているインターフェースだからです。
cs_ienumurator_t.png
IEnumerator<T> を継承すると IEnumerator も継承することになります。
実際にはIEnumerator<T> の GetEnumerator() しかまず使われません。 しかし、両方の GetEnumerator() メソッドを実装しないとコンパイルに失敗してしまいます。

この IEnumerator 側の GetEnumerator() の実装ではよく使われる常套句的な記述があります。

実装方法

サンプルを使ってこの IEnumerator<T> の実装方法を説明していきたいと思います。 コンパイル:
 > csc IEnumerableSample_T.cs
Jagged Array と呼ばれる 配列の配列 を自作のコンテナーのサンプルとします。
cs_jagged_array.png

IEnumerableSample_T.cs (クラス定義部) :
class JaggedArray<TSource> : IEnumerable<TSource>
{
    private List<TSource>[]  _list;
        
    public JaggedArray(int rowmax)
    {
        _list = new List<TSource>[rowmax];
    }
        
    public bool Add(int row, TSource val, params TSource[] restvals)
    {
        // :
    }
        
    public IEnumerator<TSource> GetEnumerator()
    {
        foreach (List<TSource> sublist in _list)
        {
            if (sublist != null)
            {
                // 子側の配列をイテレート
                foreach (TSource val in sublist)
                {
                    yield return val;
                }
            }   
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
この IEnumerable.GetEnumerator() の定義部分がポイントです。
IEnumerable のメソッドであることを IEnumerable. で明示的に指定しています。 これがないと戻り値だけが違うメソッドのオーバーロードとなり、コンパイルエラーとなります。
定義内は IEnumerable<T> の GetEnumerator() を呼び出すだけの処理です。


次に作成した JaggedArray を実際に使ってみます。
// 結果表示用のメソッド
static string Dump<TSource>(IEnumerable<TSource> source)
{
    return "{" + string.Join(", ", source) + "}";
}

static void Main()
{
    JaggedArray<int> jagary = new JaggedArray<int>(5);
    jagary.Add(0, 1);
    jagary.Add(2, 1, 2, 3, 4);
    jagary.Add(3, 1, 2);
    jagary.Add(4, 5);

    foreach (int it in jagary)
    {
        Console.Write("{0} ", it);
    }
    Console.WriteLine("\n");

    // LINQ
    Console.WriteLine("Count = {0}", jagary.Count());
    Console.WriteLine("3 Contains ? = {0}", jagary.Contains(3));
    Console.WriteLine("Max = {0}", jagary.Max());
    Console.WriteLine("Sum = {0}", jagary.Sum());
    Console.WriteLine("Average = {0}", jagary.Average());
    Console.WriteLine("ToArray = {0}", Dump(jagary.ToArray()));
    Console.WriteLine("(Source) / 2.0 = {0}", Dump(jagary.Select(it=>it/2.0)));
}
実行結果 :
1 1 2 3 4 1 2 5 

Count = 8
3 Contains ? = True
Max = 5
Sum = 19
Average = 2.375
ToArray = {1, 1, 2, 3, 4, 1, 2, 5}
(Source) / 2.0 = {0.5, 0.5, 1, 1.5, 2, 0.5, 1, 2.5}
まず foreach で要素を順にアクセスする処理を行なっています。

その後に LINQ の演算子メソッドの幾つかを使用しています。
これは多くの演算子メソッドの中のほんの一部です。 これらを自分で実装することなく、使用することができるようになっています。


 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
26位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
8位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

05月 | 2023年06月 | 07月
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。