vaguely

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

Javaのtry-with-resourcesとC#のusingを比較する その2

はじめに

前回の続きです。Java の try-with-resources と C# の using の共通点、相違点をダラダラと見ていきますよ。

どう展開されるか

try-with-resources と using は、それぞれ内部的にはどのように展開されるのでしょうか。

まずは Java の class ファイルを見てみます。

App.java

public class App{
    public static void main(String[] args){

        try (BaseSampleClass s1 = new BaseSampleClass("class1-1")){
            System.out.println("try1-1");
        }
        try (BaseSampleClass s1 = new BaseSampleClass("class1-2")){
            System.out.println("try1-2");
        }catch (Exception e){
            System.out.println("catch1-2");
        }
    }
}

App.class

public class App {
    public App() {
    }
    public static void main(String[] args) {
        BaseSampleClass s1 = new BaseSampleClass("class1-1");
        Throwable var2 = null;

        try {
            System.out.println("try1-1");
        } catch (Throwable var19) {
            var2 = var19;
            throw var19;
        } finally {
            $closeResource(var2, s1);
        }

        try {
            s1 = new BaseSampleClass("class1-2");
            var2 = null;

            try {
                System.out.println("try1-2");
            } catch (Throwable var16) {
                var2 = var16;
                throw var16;
            } finally {
                $closeResource(var2, s1);
            }
        } catch (Exception var18) {
            System.out.println("catch1-2");
        }
    }
}

try のみを書いた場合、 try-catch を書いた場合とで結果が異なっていますね。

C# では、 using のみを使用した場合 (try を使わなかった場合)、 try-finally が使用されています。

参照

ラムダ式における AutoCloseable(IDisposable)

次のコードは Java では問題ありませんが、 C# ではコンパイルエラーになります。

Java

// OK
try(IntStream nums = IntStream.range(0, 100)
        .map(n -> n)
        .filter(n -> n % 2 == 0)){
    // 何かの処理.
}

C♯

// CompileError
using (var nums = Enumerable.Range(0, 100)
    .Select(n => n)
    .Where(n => n % 2 == 0)){
    // 何かの処理.
}

これは、 IntStream が継承している BaseStream では AutoCloseable を継承しているのに対し、 Enumerable は IDisposable を継承していないためです。

では Stream API を使うときは、いつも try-with-resouces が必要なのか?というと、そうではないようです。

ドキュメントによると、基本的には close の必要はなく、下記のファイル操作のような、Stream API でなくてもCloseが必要なリソースを扱う場合のみ必要になるようです。

Path p = new File("test.txt").toPath();

try (Stream s = Files.lines(p, StandardCharsets.UTF_8)){
    s.forEach(System.out::println);
}
catch (IOException e){
    System.out.println(e.getLocalizedMessage());
}

なお上記のコードは C# だと以下のようになります。

File.ReadAllLines(@"test.txt", Encoding.UTF8)
    .ToList()
    .ForEach(Console.WriteLine);

あれ? using は?となりそうですが、 ReadAllLines を見てみると using が使われており、
自分では書かなくて良いよ〜、ということのようです。

2018.4.15 8:00更新

ReadAllLines は Java にもあるそうで、C# と同様に呼び出し先のメソッド内で try-with-resources が使われていました。
教えていただいたうらがみさん、ありがとうございましたm(__)m

try {
    Files.readAllLines(Paths.get("test.txt"))
               .forEach(System.out::println);
}
catch (IOException e){
    System.out.println(e.getLocalizedMessage());
}

File.cs

// Decompiled with JetBrains decompiler
// Type: System.IO.File
// Assembly: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// MVID: 65984520-5776-46EC-9044-386EC4A7B3DD
// Assembly location: /usr/lib/mono/4.5/mscorlib.dll

~省略~
public static string[] ReadAllLines(string path, Encoding encoding){
    using (StreamReader reader = new StreamReader(path, encoding))
        return File.ReadAllLines(reader);
}
~省略~

Linq で using が使えないためにこのような処理を行っているのか、このようにしているから Linq で using を使う必要がない、と判断したのかは追えていません。
(にわとりたまご的な)

また C# では IEnumerator が IDisposable を継承しているため、(必要かどうかはともかく)下記のようなことができます。

using (var n = Enumerable.Range(0, 10).GetEnumerator()){
    // 何かの処理            
}

こちらは後にも触れますが、 foreach のクリーンナップに使われるようです。

AutoCloseableとIDisposableは同じもの?

さてここまで見てきたとおり、 try-with-resources と using には共通点がかなり多いというか、ほとんど同じと言っても良さそうな気がします。

というところで気になったのが、AutoCloseable と IDisposable との違いです。
これらが対象とするクラスやその役割などは本当に同じなのでしょうか。

C♯

ドキュメントを見ると Dispose は、アンマネージ リソースに割り当てられたメモリを解放するためのものとなっています。

アンマネージ リソースというのは諸説あるようですが、 CLR が管理していないため、ガベージコレクションの対象にならないデータ、というくらいの認識で良さそう?です。

具体的にはファイル操作、HTTP通信などが該当します。

しかし、先程登場した Enumerable.Range では見た感じアンマネージ リソースは登場しないような気がします。

ということで Enumerable.Range を辿っていくと、System.Linq.Enumerable.Iterator で Disposeが定義されているようでした。

// Decompiled with JetBrains decompiler
// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// MVID: FB28F097-6905-4726-B681-ECB6DF11A20B
// Assembly location: /usr/lib/mono/4.5/System.Core.dll

~省略~
      public virtual void Dispose()
      {
        this._current = default (TSource);
        this._state = -1;
      }
      ~省略~

また C# の言語仕様を見てみると、foreach などで利用される Enumerator では Dispose が呼ばれたときに以下のように扱われるようです。

Enumerator の状態と Dispose が呼ばれたときの動作

  • before(処理前): 状態を「after」にする
  • after(処理後): 何もしない
  • running(処理中): unspecified
  • suspended( yield return で宣言して実行するまでの間?): Disposeを実行して状態をデフォルトに戻す

ここから、 IDisposable 及び Dispose の役割は、アンマネージ リソースを開放するだけではなく、データのクリーンナップという範囲でもう少し広い用途に使われているように思います。

Java

ドキュメントを見ると、 try-with-resources の対象になるデータは「プログラムでの使用が終わったら閉じられなければいけないオブジェクト」「リソースとして知られるローカル変数」のようになっており、 C# より範囲が広いように感じました。

ということで結論としては、使用目的としては大体同じ、ただし詳細や Close(Dispose) の動作には異なる点がある、というくらいでしょうか。

おわりに

同じような目的で作られ、同じように使われる機能であっても、詳細を調べてみると違っているところやこれまで気づかなかった特性を見つけることができたりして楽しいものですね。

一旦この話はここで締めますが、また特筆すべき違いなどがあればその3として書こうかなと思います。

参照

try-with-resources

AutoCloseable

Stream API

IDisposable

using

アンマネージドリソース