vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Java と C# の var をぼんやり比較

はじめに

みなさん Java10 使ってますか〜?(何)

Java10 で特に注目された(多分)新機能に、 var の導入があります。

var については後述しますが C♯ では ver.3.0 から導入されていたりします。

ただ C♯ では使えるところでは使う、位の扱いになっている(個人の感想です)のに対し、Javaではもう少し控えめに使おう、 という感じを受けました。

まぁJavaでは登場したばっかりだったり、別の型推論があったりと状況が違うところはあるためあくまで現状、 ということにはなりますが。

とはいえ、この違いはどこから来るのだろう?と思ったので調べてみることにしました。

var について

まずは var についてですが、ローカル変数の定義において、左辺に型を明示しなくても右辺の値から型を推論してくれる、というものです。

var id = 0; // int 型として扱われる.

int や string くらいならともかく、10 文字 20 文字と長くなってくると見る方も辛くなってきますね。

そのような場合でもスッキリ書ける、という利点があります。

  • JavaScript などとは異なり、 var を使うからといって定義後に別の型に変更することはできません。
  • コンパイル時に推論された型に置き換わります。
  • 右辺の型から推論するため、「var e;」 や 「var n = null;」 のようにするとエラーになります。

匿名クラス

また var が必須となる場合として、匿名クラスがあります。

Java

var person = new Object() {
    private String name;
    private int id;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public int getId(){
        return id;
    }
    public void setId(int id){
        this.id = id;
    }
};
person.setName("masanori");
person.setId(0);

System.out.println("name: " + person.getName() + " id: " + person.getId());

C♯

var person = new
{
    Name = "masanori", Id = 0,
};
Console.WriteLine("name: " + person.Name + " id: " + person.Id);

Java ではメソッドの定義が可能だったり、定義後の変数の変更が可能だったりと、言語仕様上の違いはありますが、いずれも var を使わずに定義しようとするとエラーになります。

Java は 「new Object」 となっているので一見 Object 型かな?とも思ってしまいますが。

var が使えない場合

ローカル変数であれば常に var が使えるかというと、そうではありません。
いくつかの状況ではコンパイルエラーになります。

ラムダ式 (Java, C♯)

下記のようなラムダ式では右辺の方が確定しないので var を使うことはできません。

// Compile error.
var lambdasample = () => {
   // Do something.
};

Java

var lambdasample = (Runnable)(() -> {
   // Do something   
});
Runnable lambdasample2 = ()->{
   // Do something
};

C♯

// OK
var lambdasample = (Action)(() => {
    // Do something   
});

// OK
Action lambdasample = () => {
   // Do something
};

配列の定義 (Java, C♯)

配列を定義する場合、下記のように書くとコンパイルエラーとなります。

// Compile error.
var arraySample = {0, 2};

理由は型が int[] に限定できないため、ということのようです。

Java

// OK.
var arraySample1 = new int[]{0, 2};
// OK.
int[] arraySample2 = {0, 2};

C♯

// OK. "int"は書かなくてもOK.
var arraySample1 = new []{0, 2};
// OK.
int[] arraySample2 = {0, 2};

C♯の arraySample2 で int が推論されるなら、 arraySample も良さそうな気がしないでもないですが。

継承しているクラス・インターフェースとして定義する (C♯)

例えば定義しようとしている変数が、このようなインターフェースを継承したクラスであった場合。

ISample.java

public interface ISample {
    String getName();
}

SampleClass.java

public class SampleClass implements ISample{
    private String name;

    SampleClass(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}

var を使うと、インターフェースではなくそれを実装している SampleClass として定義されます。

var sample = new SampleClass("hello");

ISample i = sample;
SampleClass s = sample;

// SampleClassとして定義される.
var s2 = sample;

この例で見るとそんなに大きな問題はないのですが、(C♯の) foreach で var を使った場合に思ったメソッドが出てこなくて???となる、といったことがまれによくありますね。

また C♯ で、(おそらく)上記の理由により、下記で SampleClass 、AnotherSampleClass がともに同じインターフェース (ISample) を継承していても下記のように書くことはできません。

// CompileError.
var samples = new[] {
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

// OK
ISample[] samples2 = {
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

// OK
var samples3 = new ISample[]{
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

ダイアモンド演算子 (Java)

Java7 で導入されたダイアモンド演算子と var を一緒に使うことはできません。

正確には使うこと自体は可能ですが、下記の場合だと型が ArrayList< Object> になってしまいます。

// ArrayList< Object>
var sampleList = new ArrayList< >();

// List< String>
List< String> sampleList2 = new ArrayList< >();

まぁダイアモンド演算子が左辺から型を推論するものである以上当然とも言えますが。

var をいつ使うべきか

では、使えるところではすべて var を使うべきでしょうか。
これは C♯ でも議論が行われていたようですし、今 Java で行われていたりもします。

で、先に自分の考えを書いてしまうと、使えるところではどんどん var を使って良いと思っています。

var を使うメリット・デメリット

とはいえまず var を使うメリット・デメリットから。

varを使うメリット

  1. 特に型名が長い場合も簡潔に書ける
  2. 変数の型を変えたい場合の修正量が少なくなる
  3. メソッドの戻り値が分かっていなくても書ける
  4. コードを書く量が少なくなる

varを使うデメリット

  1. 型の情報が失われる (特に IDE ではなく Web 上でコードを見た場合に調べづらい)
  2. 呼び出しているメソッドの戻り値が変更された場合に気づきにくい

メリット 3. は特にあまり使ったことのないメソッドなどを調べたりするのに便利ですね。

メリット 4. は他の方の意見通り、 IDE を使っているなら旨味は少ないかもしれません。
ただ、メソッド名まで型を気にせずに書ける、という点は利点かもしれません。

ドキュメントでの扱い

ドキュメントでは var をどのように使うべきと書かれているかを見てみます。

Microsoft Docs では下記のように書かれています。

  • 変数の型が割り当ての右側から明らかである場合、または厳密な型が重要でない場合は、ローカル変数の暗黙の型指定を使用します。
  • 割り当ての右側から型が明らかではない場合、var を使用しないでください。
  • 変数の型を指定するときに変数名に頼らないでください。 変数名が正しくない場合があります。
  • dynamic の代わりに var を使用しないようにしてください。
  • for ループおよび foreach ループでループ変数の型を決定するときは、暗黙の型指定を使用します。

引用元: C# のコーディング規則 (C# プログラミング ガイド) - Microsoft Docs

一方の Java についてですが、 OpenJDK の Style Guidelines を見てみます。

なんちゃって翻訳のため、詳しい内容や原文などは下記をご覧ください(..)_

変数名

var を使うことで、その変数についての左辺の情報は減少します。

それだけに適切な変数名をつける、ということがより重要になってきます。
前述のドキュメントでも触れられていますし、 var を使うべきか、という話でほぼ必ず登場します。

良い変数名を付けることを強制できることがメリット、という意見もあれば

変数名などで変なルールを作るべきでない、という話もあります。

これについては後者に賛成しますが、 var を使う・使わないに限らずわかりやすい名前を付けることは大事ですね。

型の詳細

var を使う・使わないを考える上でもう一つ重要なことが、その変数を使うにあたって型の詳細が必要かどうか、ということだと思います。

例えば C♯ でメソッドを呼び出して、その戻り値を foreach で使いたい場合、それが IEnumerable でも List でも配列でも特に問題はないと思います。

public void DoSomething()
{
    // samplesがListや配列でもOK.
        var samples = GetSamples();
    foreach (var s in samples)
    {
        // 何かの処理.
    }
}
private IEnumerable GetSamples()
{
    return Enumerable.Range(0, 10)
        .Select(n => new SampleClass(n, "masanori " + n));
}

このような場合、 var を使ったとしても可読性は下がらないのではないでしょうか。
(もしくは型を明示したとしてもあまり可読性が向上しないように思います)

一方、 float だと思っていたものが int だった場合など、型が異なると問題が発生する場合もあります。

とりわけ Effective C♯ で指摘されている、メソッドの戻り値が変更されてしまう危険性がある場合は、型名を明示しておいた方が良いかもしれません。

ということでこの項の最初の話に戻りますが、私の考えとしては、基本的には var を使い、型が正確に一致していないと問題が発生する場合や、インターフェース型として扱いたいなど必要がある場合のみ型を明示する、という感じです。

こうすることで、厳密な型が必要な箇所とそうでもない箇所のメリハリもつく気もしますし。

あと、var を使う・使わないにかかわらずその変数がどのように振る舞うのかは正しく認識しておく必要がありますね。

おわりに

結局 C♯ と Java でなぜ捉え方が違っている(ように感じる)のか、という話については、仕様として導入されてからの時間によっているような気がします。

2011年とか昔の記事では、 C♯ でも var の使用に否定的な意見も見られたので。
(C♯ では ver.3.0 に導入された話なので、今更話題にもなりにくいというのはあると思いますが)

また個人的には、 JetBrains の ReSharper や Rider がデフォルトで積極的に var をおすすめしてくるのも大きな要素だとは思いますw

今後 IntelliJ IDEA がどうなるのか、 Java が C♯ と同じような状況になるかはわかりませんが。

途中にも出てきましたが、 Java にせよ C♯ にせよ、 var を使うことが必ずしも最良、というわけではありません。

ただ、シンプルにできるところはできるだけ簡素化し、重要なところに力を注げると良いなとは思います。

参照

C♯

Java