こんばんは。
眠いのは春のせいです。いつも眠いのはきっと気のせいです。
しゅんみん は あかつき を おぼえた!
てってれー。
さて、今回は演算子オーバーロードについて見ていきます。
この機能はJavaを使っているときからうらやましかったですね。
特にベクトル計算とかしているととても欲しくなる機能です。
プリン + 醤油 = うに?
演算子オーバーロードはそのものズバリ、
「+」や「-」などの演算子の振る舞いをオーバーロードすることができる機能です。
ちょっとサンプルとして、濃度の違う食塩水を混ぜることを表現してみましょう。
こんなクラスですね。
public class Brine {
private double salt;
private double water;
public Brine(double salt, double water) {
this.salt = salt;
this.water = water;
}
public double getWeight() {
return this.salt + this.water;
}
public double getConcentration() {
return this.salt / this.getWeight();
}
}
さて、例えばint型同士の場合、当然「+」演算子で加算できるわけですが、
int ans = 5 + 3;
ユーザ定義のクラスでは当然そのままでは加算はできません。
Brine brine1 = new Brine(1.3, 5.2);
Brine brine2 = new Brine(3.5, 10.4);
Brine brine = brine1 + brine2;
※Integer型同士だとオートアンボクシングがかかって計算できちゃったりするけど、それは別の話。
※String型は特別に文字列同士の結合ができるけど、それも別の話。
なので、Javaではaddメソッドを実装することになります。
public Brine add(Brine target) {
return new Brine(this.salt + target.salt, this.water + target.water);
}
で、こうすると、
Brine brine1 = new Brine(1.3, 5.2);
Brine brine2 = new Brine(3.5, 10.4);
Brine brine = brine1.add(brine2);
System.out.println(brine.getWeight());
System.out.println(brine.getConcentration());
ちゃんと加算できます。
BigDecimalクラスなどもこの形式をとっていますね。
でも、なんとなーく読みづらいです。
そんなあなたに演算子オーバーロード。
なんと、「+」演算子の振る舞いを自分で書けてしまうのです。
public static Brine operator +(Brine a, Brine b)
{
return new Brine(a.salt + b.salt, a.water + b.water);
}
で、こうすると、
Brine brine1 = new Brine(1.3, 5.2);
Brine brine2 = new Brine(3.5, 10.4);
Brine brine = brine1 + brine2;
Console.WriteLine(brine.getWeight());
Console.WriteLine(brine.getConcentration());
ちゃんと「+」で加算できる!
素晴らしいです。Javaにもぷりーずです。
最初の方ですでにちょこっと書きましたが、演算子オーバーロードの文法は以下の通りです。
public static 戻り値の型 operator 演算子(引数)
さっきの例を持ってくるとこうですね。
public static Brine operator +(Brine a, Brine b)
operatorと演算子はくっついていても大丈夫そうです。
引数や戻り値は演算子の種類によって異なります。
例えば「++」のような単項演算子の場合、
引数は一つだけで、その型はメソッドを定義している自分自身の型になります。
戻り値の型も自分自身の型(か、その派生型…?)でないとだめですね。
public static Brine operator ++(Brine a)
「*」のような2項演算子の場合、
引数の数は二つで、どちらか片方の引数の型をメソッドを定義している自分自身の型にしなくてはなりません。
戻り値の型は自由です。
public static int operator *(Brine a, Brine b)
ちなみに引数は順番を気にしますので、
public static Brine operator +(Brine a, double[] b)
{
return new Brine(a.salt + b[0], a.water + b[1]);
}
とした場合、
Brine brine1 = new Brine(1.3, 5.2);
double[] brine2 = new[] { 1.2, 4.6 };
Brine brine3 = brine1 + brine2;
Brine brine4 = brine2 + brine1;
という風に、順番をひっくり返すと計算できなくなります。
キャスト演算子を定義
これは演算子オーバーロードの一種なのか、
はたまた別のものなのか微妙なところですがせっかくなので一緒に確認しておきます。
通常、任意の型から別の型への変換を行う場合、
親クラスへのキャストは暗黙的に、子クラスへのキャストは明示的に行うことができます。
これとは別に、int型からdouble型への変換など、
一部の値型には継承関係がなくても型変換することができるものがあります。
このような型変換の定義は演算子オーバーロードの記法と大体同じです。
明示的な型変換
明示的な型変換を定義する場合には「explicit」キーワードを利用します。
public static explicit operator 変換後の型(引数(変換前の型))
変換前の型、変換後の型のいずれかがメソッドを定義している自分自身の型でなくてはなりません。
(つまり、自身からのキャストと自身へのキャストの両方を定義できる)
public static explicit operator Brine(double[] target)
{
return new Brine(target[0], target[1]);
}
public static explicit operator double[](Brine target)
{
return new []{target.salt, target.water};
}
こんな風に定義すると、明示的なキャストが可能になります。
Brine brine1 = new Brine(1.3, 5.2);
double[] brine2 = (double[]) brine1;
brine1 = (Brine) brine2;
暗黙の型変換
明示の型変換を定義する場合には「implicit」キーワードを利用します。
public static implicit operator 変換後の型(引数(変換前の型))
キーワード以外に違いはありません。
public static explicit implicit Brine(double[] target)
{
return new Brine(target[0], target[1]);
}
public static explicit implicit double[](Brine target)
{
return new []{target.salt, target.water};
}
暗黙的に型変換が可能になります。
Brine brine1 = new Brine(1.3, 5.2);
double[] brine2 = brine1;
brine1 = brine2;
よくわからないまま使うとなんだか危ないかもしれませんね。
分数クラスを作る
せっかくなのでもう少し役に立ちそうなものを作ってみます。
まずは演算子とかは気にせずに基本ロジックを作ります。
public class Fraction
{
public long Numerator { get; private set; }
public long Denominator { get; private set; }
public Fraction(long numerator, long denominator)
{
if (denominator == 0)
{
throw new DivideByZeroException();
}
else if (denominator > 0)
{
this.Numerator = numerator;
this.Denominator = denominator;
}
else
{
this.Numerator = numerator * -1;
this.Denominator = denominator * -1;
}
this.Reduction();
}
public override string ToString()
{
return (this.Numerator == 0 || this.Denominator == 1) ? this.Numerator.ToString() : this.Numerator + "/" + this.Denominator;
}
private void Reduction()
{
long gdc = this.GDC(Math.Max(Math.Abs(this.Numerator), this.Denominator), Math.Min(Math.Abs(this.Numerator), this.Denominator));
this.Numerator /= gdc;
this.Denominator /= gdc;
}
private long GDC(long a, long b)
{
return b == 0 ? a : this.GDC(b, a % b);
}
}
負数は分子側に寄せるようにしました。
最大公約数計算はユークリッド互除法ですね。
で、まずは分数同士の加減乗除のメソッドを作ります。
こういう時の引数の変数名って適当でいいんですかね。
public static Fraction operator +(Fraction a, Fraction b)
{
long numerator = a.Numerator * b.Denominator + b.Numerator * a.Denominator;
long denominator = a.Denominator * b.Denominator;
return new Fraction(numerator, denominator);
}
public static Fraction operator -(Fraction a, Fraction b)
{
long numerator = a.Numerator * b.Denominator - b.Numerator * a.Denominator;
long denominator = a.Denominator * b.Denominator;
return new Fraction(numerator, denominator);
}
public static Fraction operator *(Fraction a, Fraction b)
{
long numerator = a.Numerator * b.Numerator;
long denominator = a.Denominator * b.Denominator;
return new Fraction(numerator, denominator);
}
public static Fraction operator /(Fraction a, Fraction b)
{
long numerator = a.Numerator * b.Denominator;
long denominator = a.Denominator * b.Numerator;
return new Fraction(numerator, denominator);
}
次に分数と整数、分数と小数の各値型との計算ができるようにしたいのですが、
演算子オーバーロードをたくさん書きたくないので、キャスト演算子を作ってしまいます。
public static implicit operator Fraction(long value)
{
return new Fraction(value, 1);
}
public static implicit operator Fraction(decimal value)
{
string[] split = value.ToString().Split('.');
if (split.Length == 1)
{
return (long) value;
}
long numerator = long.Parse(split[0] + split[1]);
long denominator = (long) Math.Pow(10, split[1].Length);
return new Fraction(numerator, denominator);
}
public static implicit operator Fraction(double value)
{
return (decimal) value;
}
intとかfloatとかは型変換されるのでまぁいいでしょう。
これで、計算する際に常に両方が分数に変換されます。
Fraction fra1 = 2.4;
Console.WriteLine(fra1);
Fraction fra2 = fra1 + 2;
Console.WriteLine(fra2);
Fraction fra3 = 2.33 + fra2;
Console.WriteLine(fra3);
他の演算子もオーバーロードした方が良いですが、まぁサンプルなので。
ToDecimalとかのメソッドはあってもいいかもしれませんね。
先生、表が上手く書けません
はてな記法の表に限界を感じるのですが、頑張ればきれいに収まるんでしょうか。
どうしてもうまくいかなくて、tableタグを書いてしまいました。
ぐえぐえ。
最近いつも書き終わってから思うのですが、ひとつの記事が長すぎますね。
もう少し短くしようと思いつつも、あれもこれも入れてしまいます。
もう少し簡潔にしよう。そうしよう。