※この記事は、AmusementCreators Advent Calendar 2017 の9日目の記事です。
※環境は、windows8.1 VS2017 言語はC#です
IComparableインターフェースの実装について書いていきたいと思います。ここではC#を用いており、基本的なC#の文法は知っているものとして書いていきますのでご注意ください。C#の文法は下のサイトを参考にしてください。
http://ufcpp.net/study/csharp/
#IComparableインターフェースとは#
詳しくは下のサイトを見てください。(日本語に翻訳したものだと翻訳がおかしい場所があるので、原文のサイトのURLを載せています)
https://msdn.microsoft.com/ja-jp/library/system.icomparable(v=vs.110).aspx
簡単に言えば、IComparable
インターフェースは比較を可能にするメソッドを提供するインターフェースです。このインターフェースは比較を行うことのできるクラスに必ず実装されています。
例えば、System
名前空間のInt32
構造体やDouble
構造体、String
クラスやBoolean
構造体がこれを実装しています(それぞれ組み込み型のint
、double
、string
、bool
に対応)。これらのクラスが数値、もしくは文字同士の比較ができるのはIComparable
インターフェースを実装しているからです。
#IComaparableインターフェースの構造#
IComparable
インターフェースは、CompareTo
メソッドのみを持ちます。よって、比較の処理はこのメソッドで行うことになります。どのように比較するかは、実装するクラスによって変わりますが、現在のインスタンスと、引数で与えられたインスタンスや値を用いて比較し各値の順序によって特定の整数値を返すという動作だけはどれも共通です。具体的には、現在のインスタンスをInstance
、Object
型の引数をObj
とすると、
(1)Instance
がObj
よりも後ろにあれば、1以上の整数値を、
(2)Instance
がObj
よりも前にあれば、-1以下の整数値を、
(3)Instance
とObj
が等しい場所にあれば、0を返す
という動作をします。
これだけではやはりなかなかピンと来ないかもしれません。筆者も初めはなかなか納得いかず、ちゃんとしたイメージを持てませんでした。
しかし「比較をするということは、ある二つの物をあるルールに従って比べて並べ替えることができる、ということ。」と考えれば、少しはわかりやすくなるかもしれません。例えば、数値同士の比較なら「数の大小」がルールに当たりますし、文字列同士なら「文字列の長さ」や「各文字の文字コードの合計」などが考えられます。
つまりCompareTo
というメソッドは、「自分自身と同種の別の物を、あるルールに従って並べたとき自分自身が別の物よりも前に並ぶ時は負の数を、後ろに並ぶ時は正の数を、同じ場所に並ぶ時は0を返す」機能を持つメソッドであると言えます。
CompareToの機能さえわかれば、実装は簡単です。次の項では、実際にIComparable
インターフェースを継承した二次元ベクトルの構造体を作成してみます。
#IComparableインターフェースの実装#
以下のコードを見てください。
public struct Vector : IComparable<Vector>
{
public int X { get; }
public int Y { get; }
public Vector(int vec1, int vec2)
{
X = vec1;
Y = vec2;
}
/// <summary>
/// ベクトルのノルムの二乗を返します。
/// </summary>
public static double Abs2(Vector vec)
{
return (vec.X) ^ 2 + (vec.Y) ^ 2;
}
public int CompareTo(Vector othervec)
{
//ここに比較のルールを記述
}
}
今回は、Vector
という名前の構造体を作成しました(System名前空間にも同様の名前の構造体がありますが、それとは全くの別物なのでご注意を)。二次元ベクトルを扱いたいので、int
型のプロパティを二つ作成しました。これがベクトルの成分に当たります。
また、Abs2
というメソッドも作成しました。このメソッドでは、ベクトルのノルム(ベクトルの大きさ)の二乗を返します。なぜ作成したかについては後々わかります。
実装しているインターフェースはIComparable<Vector>
というインターフェースですが、これは先ほど説明したIComparable
インターフェースのジェネリック版で、機能的にはIComparable
とほぼ変わりません。
ただし、CompareTo
メソッドのとれる引数がVector
型のみという制限が付きます(このように、インターフェースの中にはジェネリックなものと非ジェネリックなものがあります。基本的にはジェネリックなインターフェースを使うことが推奨されているようです)。
##CompareToメソッドの作成##
さて、早速CompareTo
メソッドの中身を記述していきたいと思いますが、その前に、先ほども書いたように比較するには何らかの「ルール」がなくてはいけません。そのためまずどのように比較するのかのルールを決めましょう。
例えば次のようなルールが考えられます
(1)二つのベクトルの大きさで比較
(2)基本ベクトル(成分が全て1のベクトル)と各ベクトルのなす角で比較
(3)各ベクトルの成分の大きい方同士で比較
せっかくなので、この3つのルールををそれぞれ記述してみましょう。
###(1)二つのベクトルの大きさで比較###
public int CompareTo(Vector othervec)
{
return Abs2(this).CompareTo(Abs2(othervec));
}
パッと見ても、何をしているのかわかりにくいと思いますが、ここではDouble
構造体で実装されているCompareTo
メソッドを用いています。このルールでの比較は結局のところdouble
型の数値の比較であるので、CompareTo
を用いています。もちろん比較演算子を用いても記述することはできますが、CompareTo
メソッドを用いると上のように1行で済むのでコードを短くすることができます。
###(2)基本ベクトルとのなす角の比較###
public int CompareTo(Vector othervec)
{
double f(Vector vec1, Vector vec2) => (vec1.X * vec2.X + vec1.Y * vec2.Y) / Math.Sqrt(Abs2(vec1) * Abs2(vec2));
var e = new Vector(1, 0);//単位ベクトル
return f(this, e).CompareTo(f(othervec, e));
}
ここではx方向の基本ベクトルを基準とした各ベクトルのなす角の余弦を比較しています。
余弦で比較しているのは、角と余弦はともに0~πの範囲で単調増加であるため共に大小関係が等しくなるからです。返り値の構造は、(1)と同じです。
ここで、上のコードの3行目に見慣れないコードがあるかと思います。これはローカル関数と呼ばれるもので、関数の内部に関数を記述することができます。詳しくは下のサイトを参考にしてください。
http://ufcpp.net/study/csharp/functional/fun_localfunctions/
このローカル関数の内部では、与えられたベクトルから余弦を計算して返す機能を持っています。
###(3)各ベクトルの成分の大きい方同士で比較###
public int CompareTo(Vector othervec)
{
return Math.Max(this.X, othervec.X).CompareTo(Math.Max(this.Y, othervec.Y));
}
返り値は(1)、(2)と同じです。また、Max
メソッドは、二つの引数のうち大きい方を返す関数です。
#まとめ#
比較機能を提供する、と書くとなんだか難しそうでしたが、これまで見てきたように行っていることはそこまで複雑ではなかったと思います。
このメソッドを応用すれば、例えばコレクションクラスに対して実装すると各要素ごとに独自のソーティングを行うこともできます。また、比較演算子のオーバーロードで新たに比較演算子で要素の比較を行うこともできます。
この記事を読んで少しでも理解の助けになってくれれば嬉しいです。
参考にしたサイト:
http://garicchi.hatenablog.jp/entry/2014/09/15/200000
https://msdn.microsoft.com/ja-jp/library/system.icomparable(v=vs.110).aspx