はじめに
前回のクイズには挑戦していただけましたか?
コンピュータに向かわずにコードについて考える訓練は、プログラミングのための思考能力を高めるのにも役立ちます。さあ、今回も、Delphi、C++、Java、C#、PHP、Rubyの6言語を取り上げ、同じような動作を記述していながら、結果が異なるケースを紹介し、言語ごとの挙動の違いを考察していきましょう。
前回の記事
最初のクイズ(割り算)
問題
最初のクイズは割り算に関するものです。6つの言語を使って、きれいに割り切れない割り算「2007÷11÷21」を試してみました。しかし、1つだけ出力結果が違うものがあります。それは、どの言語でしょうか?
program Project1; {$APPTYPE CONSOLE} begin Write(2007 div 11 div 21); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { cout << 2007/11/21; return 0; }
package project1; public class Project1 { public static void main(String[] args) { System.out.print(2007/11/21); } }
namespace project1 { public class Project1 { public static void Main() { System.Console.Write(2007/11/21); } } }
<?php
echo 2007/11/21;
?>
print 2007/11/21
解説
正解は「PHP」。
PHPでは、期待した「8」は出力されず、少数点「8.6883116883117」が出力されてしまいます。それ以外の言語では正しく「8」が出力されます。これは一体どういうことでしょうか。
Delphi/C++/Java/C#/Rubyでは結果について特別に意識することはないでしょう。整数同士の割り算の結果、小数点は切り捨てられ、整数値が得られます。しかしPHPでは、整数値同士のであっても割り切れない場合には小数点が返されます。
少数ではなく整数値「8」を取得したい場合には、intへの明示的なキャストが必要になります。以下に示すようなコードにより、期待した「8」が出力されます。
<?php
echo (int)((int)(2007/11)/21);
?>
引き算
問題
今度は引き算です。6種類の言語を使って、とてもカンタンな引き算「128-256」の値を確認してみましたが、1つだけ出力結果が異なるものがあります。さて、それはどれでしょうか?
program Project1; {$APPTYPE CONSOLE} var x: Shortint; begin x := 128-256; Write(x); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { __int8 x = 128-256; cout << x; return 0; }
public class Project1 { public static void main(String[] args) { byte x = 128-256; System.out.print(x); } }
namespace project1 { public class Project1 { public static void Main() { sbyte x = 128-256; System.Console.Write(x); } } }
<?php
$x = 128-256;
echo $x;
?>
x = 128-256 print x
解説
正解は「C++」。
C++では、期待した「-128」は出力されません。それ以外の言語では正しく「-128」が出力されます。Delphi/Java/C#/Ruby/PHPでは結果について特別意識することはないでしょう。C++では、通常の「int型」ではなく「__int8型」を使用しているのが気にかかります。
確かに「__int8型」は8bitの範囲(-128~127)の値を扱えますから、「-128」と出力されるべきでしょう。しかし一般的な実装では「__int8型」は「char型」であることから、「-128」を「文字(16進数で0x80)」として出力する処理が実行されます。この結果、「-128」とは出力されません。
問題のコードでは、short型などへの明示的なキャストが必要になります。以下に示すようなコードにより、期待した「-128」が出力されます。
cout << (short)x;
なお、C#では「byte型」ではなく「sbyte型」を使用しています。C#の「byte型」は符号無しですので、符号付きの値を処理するには「sbyte型」が必要となる点に注意が必要です。
ゼロを比較
問題
さて、浮動小数点について。同じに見える2つのゼロ(+0.0、-0.0)を比較するコードを6つの言語で試してみました。今回もやはり1つだけ出力結果が違うものがありました。それはどれでしょうか?
program Project1; {$APPTYPE CONSOLE} var a: Extended; begin a := 0.0; if a = -0.0 then Write('equal'); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { double a = 0.0; if( a == -0.0 ) cout << "equal"; return 0; }
package project1; public class Project1 { public static void main(String[] args) { Double a = 0.0; if( a.equals(-0.0) ) System.out.print("equal"); } }
namespace project1 { public class Project1 { public static void Main() { double a = 0.0; if( a == -0.0 ) System.Console.Write("equal"); } } }
<?php $a = 0.0; if( $a == -0.0 ) echo 'equal'; ?>
a = 0.0 if a == -0.0 print "equal" end
解説
正解は「Java」。
Javaでは、期待した「equal」は出力されません。それ以外の言語では正しく「equal」が出力され、2つのゼロ(+0.0、-0.0)が等しいことが確認できます。一般的に、この2つのゼロ(+0.0、-0.0)は等しいと扱うのが自然だと思いますが、Javaはマイナスのゼロ「-0.0」に対応しています。
問題のコードがプリミティブ型の「double」ではなく、ラッパータイプの「java.lang.Doubleクラス」を利用している点に注目してください。java.lang.Doubleクラスのコンストラクタは、doubleの即値「-0.0」や文字列「”-0.0”」を特別な値として受け取ることができ、2つのゼロ(+0.0、-0.0)を区別することが可能です。
問題のコードはJava SE 5から導入されたボクシング変換を併用しているので、以下に示すようにJava SE 5以前のコードに置き換えて考えると分かりやすいでしょう。
Double a = new Double(+0.0); if( a.equals(new Double(-0.0)) ) System.out.print("equal");
もちろん、DoubleクラスのdoubleValue()メソッドを使って「equal」と出力することもできます。
Double a = new Double(+0.0); Double b = new Double(-0.0); if( a.doubleValue() == b.doubleValue() ) System.out.print("equal");
ビット演算と…
問題
次は、ビット演算について。テストコードを6つの言語で書いてみましたが、今回もやはり1つだけ出力結果が違うものがありました。それはどれでしょうか?
program Project1; {$APPTYPE CONSOLE} var a: Integer; begin a := 2 and 1 shl 1; Write(a); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { int a = 2 & 1 << 1; cout << a; return 0; }
package project1; public class Project1 { public static void main(String[] args) { int a = 2 & 1 << 1; System.out.print(a); } }
namespace project1 { public class Project1 { public static void Main() { int a = 2 & 1 << 1; System.Console.Write(a); } } }
<?php
$a = 2 & 1 << 1;
echo $a;
?>
a = 2 & 1 << 1 print a
解説
正解は「Delphi」。
Delphiだけが「0」を出力し、それ以外の言語は「2」と出力します。実は今回のコード、ビット演算自体に問題がある訳ではありません。普通に考えると、「まず2と1のANDは0、次にその0を1ビット左にシフトして…やはり0」という結果が得られると思いがちですが、演算子には優先順位があります。
問題のコードのように評価する順序を明示的に示していない場合、優先順位が高いものから順に評価されます。Delphi以外の言語では、ビットシフトの演算子「<<」の方が優先順位が高く、先に評価されます。一方Delphiでは「and」と「shl」は順位が同じであるため左から順に、つまり「and」が先に評価されます。このようなうっかりミスを防ぐためにも、言語の種類に限らず「(…)丸括弧」を明示的に指定すべきでしょう。
a := 2 and (1 shl 1);
int a = 2 & (1 << 1);
コンストラクタの連鎖とデフォルト引数
問題
最後はちょっと難しくなります。クラス継承時のコンストラクタの連鎖と、メソッドのデフォルト引数について。これも6つの言語でテストコードを書いてみました。言語の中には、デフォルト引数の機能を持たないものもありますので、それらもできるだけ似せて書いたつもりです。でも、やはり1つだけ結果の異なるものがありました。それはどの言語でしょうか?
program Project1; {$APPTYPE CONSOLE} type TParent = class public constructor Create(x: Integer = 1); end; TChild = class(TParent) public constructor Create(x: Integer); end; constructor TParent.Create(x: Integer); begin Write(x); end; constructor TChild.Create(x: Integer); begin inherited Create; Write(x); end; begin TChild.Create(2); end.
#include <iostream> using namespace std; class TParent { public: TParent(int x = 1) { cout << x; } }; class TChild : public TParent { public: TChild(int x) : TParent() { cout << x; } }; int main(int argc, char* argv[]) { new TChild(2); return 0; }
package project1; class TParent { public TParent() { this(1); } public TParent(int x) { System.out.print(x); } } class TChild extends TParent { public TChild(int x) { System.out.print(x); } } public class Project1 { public static void main(String[] args) { new TChild(2); } }
namespace project1 { class TParent { public TParent() : this(1) { } public TParent(int x) { System.Console.Write(x); } } class TChild : TParent { public TChild(int x) : base() { System.Console.Write(x); } } public class Project1 { public static void Main() { new TChild(2); } } }
<?php class TParent { public function __construct($x = 1) { echo $x; } } class TChild extends TParent { public function __construct($x) { parent::__construct(); echo $x; } } new TChild(2); ?>
class TParent def initialize(x = 1) print x end end class TChild < TParent def initialize(x) super print x end end TChild.new(2)
解説
正解は「Ruby」。
Ruby以外の言語では、期待した「12」という出力が得られますが、Rubyでは「22」と出力されます。問題のコードは、「子クラス」のコンストラクタが「直接の親クラス」のコンストラクタを呼び出す処理が、以下のような順番で進むことを期待しています。
- TChildクラスのインスタンスを生成
- TChildクラスのコンストラクタに「2」が渡される
- TChildクラスのコンストラクタがTParentクラスのデフォルトコンストラクタを呼び出す
- TParentクラスのデフォルトコンストラクタは「1」を受け取る
しかし実際には、RubyだけがTParentクラスのデフォルトコンストラクタに「2」を受け取ってしまっています。どこに問題が隠れているのでしょうか? 実はRubyでは、親クラスのメソッド(コンストラクタを含む)を呼び出す際、括弧と引数が省略された「super」と記述すると、子クラス自身が引数として受け取った変数が自動的に親クラスのメソッドに渡されます。つまり、問題のコードは以下のように記述したのと同じになります。
class TChild < TParent def initialize(x) super(x) # superと同じ print x end end
もちろん、以下のように括弧「()」を明示的に記述することにより、他の言語と同じように動作させることもできます。
class TChild < TParent def initialize(x) super() # super()はsuper(1)と同じ print x end end
なお、Delphiの「inherited」にはRubyと同じ特徴があります。問題のコードは、実際には以下のコードと同じになります。
constructor TChild.Create(x: Integer); begin inherited Create(1); // inherited Create; と同じ Write(x); end;
Rubyと同様に「inherited;」とだけ記述すると、子クラス自身が引数として受け取った変数が自動的に親クラスのメソッドに渡されます。
constructor TChild.Create(x: Integer); begin inherited; // inherited Create(x); と同じ Write(x); end;