白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

Java使いがC#を勉強する その⑨ 演算子オーバーロード

こんばんは。

眠いのは春のせいです。いつも眠いのはきっと気のせいです。

しゅんみん は あかつき を おぼえた!
てってれー。


さて、今回は演算子オーバーロードについて見ていきます。
この機能は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());        // 20.4
System.out.println(brine.getConcentration()); // 0.235294117647059

ちゃんと加算できます。
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());        // 20.4
Console.WriteLine(brine.getConcentration()); // 0.235294117647059

ちゃんと「+」で加算できる!
素晴らしいです。Javaにもぷりーずです。

 

オーバーロード可能な演算子

 
オーバーロード可能な演算子について詳しく見ていきます。
こちらから抜粋です。

オーバーロードされた演算子 (C# プログラミング ガイド)


種類 演算子 オーバーロード メモ
単項演算子 +、 -、 !、 ~、 ++、 --、 true、 false trueとfalseはペアでオーバーロードする必要がある。
2項演算子 +、 -、 *、 /、 %、 &、 |、 ^、 <<、 >>
比較演算子 ==、 !=、 <、 >、 <=、 >= それぞれペアでオーバーロードする必要がある。
条件付き論理演算子 &&、 || ×※ それぞれ、&、 |をオーバーロードすることで利用可能。
代入演算子 +=、 -=、 *=、 /=、 %=、 &=、 |=、 ^=、 <<=、 >>= ×※ +をオーバーロードして+=を利用するなど可能。
配列のインデックス演算子 [] ×※ インデクサを定義可能。
キャスト演算子 (T)x ×※ 変換演算子を定義可能。
演算子の追加みたいな感じ)
=、.、?:、??、->、=>、f(x)、as、checked、unchecked、default、delegate、is、new、sizeof、typeof ×

大きく分けると、

・単独でオーバーロード可能な演算子
・ペアでオーバーロード可能な演算子(片方のみ実装は不可)
・ほかの演算子オーバーロードすることで、間接的に利用できる演算子
オーバーロード不可の演算子

の4種類がありそうですね。

 

演算子オーバーロードの文法

 

最初の方ですでにちょこっと書きましたが、演算子オーバーロードの文法は以下の通りです。

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; // こっちはOK
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)
    {

        // 分母0は無理
        if (denominator == 0)
        {
            throw new DivideByZeroException();  // ArgumentExceptionの方がいい?                
        }
        // 分母が正数の場合、そのまま入れる
        else if (denominator > 0)
        {
            this.Numerator = numerator;
            this.Denominator = denominator;                
        }
        // 分母が負数の場合、符号を反転
        else
        {
            this.Numerator = numerator * -1;
            this.Denominator = denominator * -1;
        }

        // 必ず約分する
        this.Reduction();
    }

    // 文字列表現(n/m形式で表示)
    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); // 12/5

Fraction fra2 = fra1 + 2;
Console.WriteLine(fra2); // 22/5

Fraction fra3 = 2.33 + fra2;
Console.WriteLine(fra3); // 673/100

他の演算子オーバーロードした方が良いですが、まぁサンプルなので。
ToDecimalとかのメソッドはあってもいいかもしれませんね。

先生、表が上手く書けません

 
はてな記法の表に限界を感じるのですが、頑張ればきれいに収まるんでしょうか。
どうしてもうまくいかなくて、tableタグを書いてしまいました。
ぐえぐえ。

最近いつも書き終わってから思うのですが、ひとつの記事が長すぎますね。
もう少し短くしようと思いつつも、あれもこれも入れてしまいます。
もう少し簡潔にしよう。そうしよう。