白猫のメモ帳

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タグを書いてしまいました。
ぐえぐえ。

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

Java使いがC#を勉強する その⑧ 例外処理

こんばんは。

平穏無事な日々を送りたいわたしです。
でも、世の中には思いがけないことも多いものです。
そんなわけで今日は例外処理です。

例外の比較


ひとまずJavaC#の例外を比較してみましょう。

一般的な例外(Javaでは非検査例外)

説明 C# Java
null値の参照型変数を参照しようとした NullReferenceException NullPointerException
境界外のインデックスを使用して配列、
コレクションなどの要素にアクセスしようとした
IndexOutOfRangeException ※1 IndexOutOfBoundsException
├ArrayIndexOutOfBoundsException
└StringIndexOutOfBoundsException
算術計算で例外的条件が発生した ArithmeticException
├DivideByZeroException
├NotFiniteNumberException
└OverflowException
ArithmeticException
不正な引数、または不適切な引数をメソッドに渡した ArgumentException
├ArgumentNullException
└ArgumentOutOfRangeException
IllegalArgumentException
無効なキャストが行われた InvalidCastException ClassCastException
オブジェクトの現在の状態に対して、不正または不適切なときにメソッドが呼び出された InvalidOperationException IllegalStateException
文字列を数値型に変換しようとしたとき、文字列の形式が正しくない ※2 FormatException NumberFormatException ※3
要求されたオペレーションがサポートされていない NotSupportedException UnsupportedOperationException
間違った型の要素を配列に格納しようとした ArrayTypeMismatchException ArrayStoreException
リソースが欠落している MissingManifestResourceException MissingResourceException

※1 String.RemoveなどはArgumentOutOfRangeExceptionになる
※2 FormatExceptionの範囲はもう少し広く、「引数の書式が仕様に一致していない」ことを表す
※3 NumberFormatExceptionはIllegalArgumentExceptionのサブクラス

JavaではErrorとして扱われるもの

説明 C# Java
入れ子になったメソッド呼び出しが多くなりすぎ、
実行スタックがオーバーフローした
StackOverflowException StackOverflowError
プログラムの実行を継続するためのメモリが不足している OutOfMemoryException OutOfMemoryError


C#にはあるけどJavaにはなさそうなもの

説明 C#
特定のプラットフォームで機能が実行されない PlatformNotSupportException
Dispose済みのオブジェクトで操作が実行される ObjectDisposedException
要求されたメソッドまたは操作が実装されない ※4 NotImplementedException
コレクションに該当するキーが無い ※5 KeyNotFoundException

※4 Javaではテンプレートでの自動挿入時にはUnsupportedOperationExceptionを使う
※5 Javaではキーがない場合にはnullを返す

JavaにはあるけどC#にはなさそうなもの

説明 Java
オブジェクトの並行変更を検出したメソッドによって、
そのような変更が許可されていない ※6
ConcurrentModificationException
この列挙にそれ以上の要素がない ※6 NoSuchElementException

※6 C#ではInvalidOperationExceptionが発生する


リフレクション関連とかI/O関連とか見ていくともっとたくさんあるとは思うのですが、
ひとまずはこのくらいで。

検査例外と非検査例外


C#には非検査例外がありません。というかJavaには検査例外があります。
この一言で終わってしまう気もするのですが、せっかくなのでもう少し踏み込んでみます。

まず先に言っておくと、Java以降に誕生した主要な言語に検査例外を持つものはほとんどありません。
というかわたしはJavaしか知らないのですが、他にもあるんでしょうか。

非検査例外のメリット

非検査例外の良いところはコンパイル時点でどのような例外が発生するかを想定できることです。
つまり、メソッドを呼び出すときなどに
「いいですか、私を呼んだら例外出るかもしれませんよ。覚悟してくださいね。」
と呼び出す相手に脅してお知らせしてくれるわけです。

これは単にプログラマの心構えの問題だけではなくて、
例外のハンドリングの責任を誰が持つか、というのを明確にすることができます。

つまり、発生した例外を自分で処理できる場合にはその例外の存在を上の階層に意識させる必要がないですし、
処理しきれない場合にはthrows節を使って上の階層に明確に例外発生の可能性を通知することができるわけです。

非検査例外のデメリット

当然メリットの裏返しがデメリットにもなるわけで、
メソッドが投げる例外を想定できないということが挙げられます。

これだけであれば設計思想とか趣味の範囲で済むのかもしれませんが、
致命的なのがシグネチャの変更がメソッドの互換性とか拡張性を著しく下げる場合があるということです。

例えば、検査例外であるSampleExceptionを投げるmethod3の例外処理を、二階層上のmethod1で行う場合を考えます。

void method1() {
    try {
        method2();
    } catch (SampleException e) {
        System.out.println("例外だよ");
    }
}

void method2() throws SampleException {
    method3();
}

void method3() throws SampleException {
    throw new SampleException();
}

method2を書く際にはSampleExceptionはこの階層では処理しきれないので上に渡そうと考えるわけですが、
method3が投げる例外の種類が増えた場合、method2もthrows節を変更しなければなりません。
しかし、method2には実質的には何も変更はないのです。

void method1() {
    try {
        method2();
    } catch (SampleException e) {
        System.out.println("例外だよ");
    } catch (SampleException2 e) {
        System.out.println("例外2だよ");
    }
}

void method2() throws SampleException, SampleException2 {
    method3();
}

void method3() throws SampleException, SampleException2 {
    if (Math.random() >= 0.5) {
        throw new SampleException();
    } else {
        throw new SampleException2();
    }
}

ネストがもっと深ければ、軽微な変更でもかなりの修正コストを要することになります。

これが面倒で複数の例外をラップして投げたりすることによって、
結果的にはどんな例外が飛んでくるのかよくわからなくなったりするのです。

もういっそ上がってくる例外をそのまま上に流すよって意味で新しい文法作ったらいいんじゃないでしょうか。
throwsに似せた感じでthroughとかどうです?

void method2() through {
    method3();
}

もちろんIDEでちゃんとチェックしてくださいね。

好きだよ

 
なんだか検査例外に対して否定的になってしまいましたが、わたしは検査例外好きですよ。
何が出るかなお楽しみなメソッドは怖いのです。はい。

でも、C#にはないんですよね。
ちょっと悲しい。

C#を勉強する 小ネタ コンソール邪魔

こんばんは。

気が付けばもう3月ですね。
春の足音が聞こえ・・・ない。寒い。

今回はちょっとした小ネタです。

VisualStudioを使うようになって、Eclipseとの違いにいろいろ戸惑っています。
特にショートカットは手が覚えているので、しょっちゅう変なキーを押している気がします。
そして戸惑っていたのがコンソールだったのですが、割と快適になったので手順をメモっておきます。

C#で「Hello World」を試すためには以下の手順で行うことになります。

1. 新規プロジェクトとして「コンソールアプリケーション」を作成する

2. 以下のコードを記述する

3. F5キーでプロジェクトを実行する

using System;
namespace HelloWorld
{
    class Hello 
    {
        static void Main() 
        {
            Console.WriteLine("Hello World!");
        }
    }
}

ちかっ(なんかみえる)ぱっ(すぐさまきえる)

なるほど。これはあれか、プログラミング能力と動体視力を同時に鍛える的なやつだな!
・・・そんなわけないので、普通はこうするようです。

using System;
namespace HelloWorld
{
    class Hello 
    {
        static void Main() 
        {
            Console.WriteLine("Hello World!");
            Console.ReadKey(); // ReadLineとかでも可
        }
    }
}

要するに終わるとすぐ閉じてしまうので、入力待ちにしておこうというわけです。

f:id:Shiro-Neko:20170301105108j:plain

なにかわたしが望んでいるのと違う・・・。
別になんとかプロンプトとか出てこなくて良いのですよ。

というわけで出ないようにします。

1. プロジェクトを右クリック-「プロパティ」

2. 「アプリケーション」の「出力の種類」を「Windowsアプリケーション」に変更

f:id:Shiro-Neko:20170301104840j:plain

すると、ReadKeyとかなくても黒い画面が出てこなくなりました。やったね!
そして出力ウィンドウには「Hello World!」の文字(とゆかいな仲間たち)が!

f:id:Shiro-Neko:20170301104000j:plain

読みづらい・・・。
ので出力ウィンドウで右クリックして、
「例外メッセージ」と「プログラム出力」以外のチェックを外します。

f:id:Shiro-Neko:20170301104245j:plain

もう一回実行すると、まぁすっきり!

f:id:Shiro-Neko:20170301104328j:plain

快適になりましたとさ。
めでたしめでたし。