はじめに
職務上、さまざまなプログラミング言語を扱うことがあるのですが、言語によって文法や挙動の違いに戸惑ったり、うっかりミスをしたりすることがあります。でも、1つの言語だと当たり前に思ってしまうことも、複数の言語を並べることで、逆によく見えてくることもあります。この記事では、Delphi、C++、Java、C#、PHP、Rubyの6言語を取り上げ、同じような動作を記述していながら、結果が異なるケースを紹介し、言語ごとの挙動の違いを考察します。
最初のクイズ(変数のスコープ)
問題
最初のクイズは、変数のスコープに関するものです。グローバル変数やstaticなメンバを使ったコードを6つの言語で書いてみましたが、1つだけ出力結果が違うものがありました。それはどれでしょうか?
program Project1; {$APPTYPE CONSOLE} var x: Integer; procedure test; begin Write(x); end; begin x := 1; test; end.
#include <iostream> int x; void test() { std::cout << ::x; } int main(int argc, char* argv[]) { ::x = 1; test(); return 0; }
package project1; public class Project1 { public static void test() { System.out.print(x); } public static void main(String[] args) { x = 1; test(); } private static int x; }
namespace project1 { public class Project1 { public static void test() { System.Console.Write(x); } public static void Main() { x = 1; test(); } private static int x; } }
<?php global $x; function test() { echo $x; } $x = 1; test(); ?>
def test print $x end $x = 1 test
解説
正解は「PHP」。
PHPでは、期待した「1」は出力されません。それ以外の言語では正しく「1」が出力されます。Delphi/C++/Java/C#については特にうっかりミスを起こすことはないでしょう。Rubyでは、グローバル変数を使用する場合には、変数の先頭に「$」を追加する必要があります。PHPでは、変数のスコープは関数の中と外とで異なります。そのため、以下に示すコードのように、グローバル変数を利用する場合には関数の中でもglobalの宣言が必要になります。これで期待した「1」が出力されます。
function test() { global $x; echo $x; }
文字の長さ
問題
続いて文字の長さについて。6つの言語で「あ」の長さを取得してみました。しかし、やはり1つだけ出力結果が違うものがありました。それはどれでしょうか?
それぞれ、ソースファイルの文字コードやコンパイルオプションなどは正しく設定されているハズなのですが…。
program Project1; {$APPTYPE CONSOLE} var str: WideString; begin str := 'あ'; Write(Length(str)); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { wstring str = L"あ"; cout << str.length(); return 0; }
package project1; public class Project1 { public static void main(String[] args) { String str = "あ"; System.out.print(str.length()); } }
namespace project1 { public class Project1 { public static void Main() { string str = "あ"; System.Console.Write(str.Length); } } }
<?php ini_set('mbstring.internal_encoding', 'UTF-8'); $str = 'あ'; echo mb_strlen($str); ?>
$KCODE='u' str = 'あ' print str.length
解説
正解は「Ruby」。
Rubyでは、期待した「1」ではなく「3」が出力されます。それ以外の言語では正しく「1」が出力されます。Delphi/C++/Java/C#では基本的に1文字を16bitのUnicodeで管理することが可能です。特にうっかりミスを起こすことはないでしょう。
PHPでは、文字を処理する際のエンコード(UTF-8やSJISなど)を設定することができます。問題のコードでは、UTF-8でエンコードされた「あ」をmb_strlen関数を利用して正しく1文字として認識しています。しかし、Rubyでは文字列の長さを取得するlengthメソッドはUTF-8に対応しておらず、バイト単位のサイズが返されます。そのため「3」が出力されました。
そこでRubyでは、以下に示すコードのようにUTF-8に対応したライブラリ「jcode」または「RailsのActiveSupport」を利用することで正しく「1」が出力されます。
require 'jcode' $KCODE='u' str = 'あ' print str.jlength
require 'rubygems' require 'active_support' $KCODE='u' str = 'あ' print str.chars.length
文字列の足し算と比較
問題
次は、文字列の足し算と比較(=、==、===、equals)です、6つの言語で試してみましたが、やはり、1つだけ出力結果が異なるものがあります。どれでしょう?
program Project1; {$APPTYPE CONSOLE} var s1,s2: string; begin s1 := ‘ab’; s2 := ‘a’ + ‘b’; if s1 = s2 then Write(’equal’); end.
#include <iostream> using namespace std; int main(int argc, char* argv[]) { string s1 = "ab"; string s2 = string("a") + string("b"); if( s1 == s2 ) cout << "equal"; return 0; }
package project1; public class Project1 { public static void main(String[] args) { String s1 = "ab"; String s2 = "a" + "b"; if( s1.equals(s2) ) System.out.print("equal"); } }
namespace project1 { public class Project1 { public static void Main() { string s1 = "ab"; string s2 = "a" + "b"; if( s1 == s2 ) System.Console.Write("equal"); } } }
<?php $s1 = "ab"; $s2 = "a" + "b"; if( $s1 === $s2 ) echo "equal"; ?>
s1 = "ab" s2 = "a" + "b" if s1 === s2 print "equal" end
解説
正解は「PHP」。
PHPでは、期待した「equal」は出力されません。それ以外の言語では正しく「equal」が出力され、2つの文字列が等しい内容であることが確認できます。Delphi/C++/Java/C#/Rubyについては特にうっかりミスを起こすことはないでしょう。
PHPでは、文字列同士を結合する演算子は「+」ではなく「.(ドット)」です。問題のコードのように「+」を使用して文字列同士を結合しても特にエラーは発生しません。「.(ドット)」を使うことで期待した「equal」が出力されます。
$s2 = "a" . "b";
なお、Javaでは文字列型を含むクラス型の変数同士の等値性を判定する場合、「==演算子」ではなく「equalsメソッド」を使用します。これとは対照的に、Delphiの「=演算子」、C++の「==演算子」、C#の「==演算子」は、文字列においては変数の参照先を比較するのではなく、文字列の内容の等値性を判定する処理へと置き換えられます。この点、注意が必要です。
文字列で出力
問題
次は、インスタンスを文字列として表現して出力する場合です。この中でも1つだけ出力結果が違うものがあります。それは、どの言語でしょうか?
program Project1; {$APPTYPE CONSOLE} type TMyClass = record class operator Explicit(x: TMyClass): string; end; class operator TMyClass.Explicit(x: TMyClass): string; begin Result := ’TMyClass’; end; var obj: TMyClass; begin Writeln(string(obj)); end.
#include <iostream> using namespace std; class TMyClass { friend ostream& operator <<(ostream& os, const TMyClass& obj) { os << "TMyClass"; return os; } }; int main(int argc, char* argv[]) { cout << TMyClass() << endl; return 0; }
package project1; class TMyClass { public String toString() { return "TMyClass"; } } public class Project1 { public static void main(String[] args) { System.out.println(new TMyClass()); } }
namespace project1 { class TMyClass { public string toString() { return "TMyClass"; } } public class Project1 { public static void Main() { System.Console.WriteLine(new TMyClass()); } } }
<?php class TMyClass { public function __toString() { return "TMyClass"; } } echo (string)new TMyClass() . "\n"; ?>
class TMyClass def to_s "TMyClass" end end puts TMyClass.new
解説
正解は「C#」。
C#では、期待した「TMyClass」ではなく「project1.TMyClass」が出力されます。それ以外の言語では正しく「TMyClass」が出力されます。これは一体どうしたというのでしょう。
Delphi(record型のみ)では明示的なキャストが可能ですし、C++では演算子オーバーロードを定義することが可能です。JavaではtoString()メソッドを、PHPでは__toString()メソッドを、Rubyではto_sメソッドをオーバーライドすることにより、文字列としての表現方法を独自に実装することが可能です。もちろんC#でも「ObjectクラスのToString()メソッド」をオーバーライドすることで同様の処理が可能です。
しかし問題のコードでは、オーバーライドが正しく行われていません。コンパイル時には警告もエラーも発生しませんでした。C#ではメソッドの大文字・小文字を区別しますし、メソッドのオーバーライドにはoverride修飾子が必要なのです。
このため「ObjectクラスのToString()メソッド」が呼び出され、その既定の実装である「クラスの完全限定名」が文字列として返されたのです。正しくは以下のように記述します。
class TMyClass { public override string ToString() { return "TMyClass"; } }
メソッドのオーバーライド
問題
次は、ちょっと難しくなります。6種類の言語を使って「親クラス」と「子クラス」を定義して、メソッドのオーバーライドの処理を書いてみました。同じような動作を期待したのですが、1つだけ出力結果が違うものがありました。それは、どの言語でしょうか?
program Project1; {$APPTYPE CONSOLE} type TParent = class public constructor Create; protected procedure foo; virtual; end; TChild = class(TParent) public constructor Create; protected procedure foo; override; end; constructor TParent.Create; begin Self.foo; end; procedure TParent.foo; begin Writeln(’TParent#foo’); end; constructor TChild.Create; begin inherited; end; procedure TChild.foo; begin inherited; Writeln(’TChild#foo’); end; begin TChild.Create; end.
#include <iostream> using namespace std; class TParent { public: TParent(){ this->foo(); } protected: virtual void foo() { cout << "TParent#foo" << endl; } }; class TChild : public TParent { public: TChild() : TParent(){ } protected: virtual void foo() { TParent::foo(); cout << "TChild#foo" << endl; } }; int main(int argc, char* argv[]) { new TChild(); return 0; }
package project1; class TParent { public TParent() { this.foo(); } protected void foo() { System.out.println("TParent#foo"); } } class TChild extends TParent { public TChild() { } protected void foo() { super.foo(); System.out.println("TChild#foo"); } } public class Project1 { public static void main(String[] args) { new TChild(); } }
namespace project1 { class TParent { public TParent() { this.foo(); } protected virtual void foo() { System.Console.WriteLine("TParent#foo"); } } class TChild : TParent { public TChild() : base() { } protected override void foo() { base.foo(); System.Console.WriteLine("TChild#foo"); } } public class Project1 { public static void Main() { new TChild(); } } }
<?php class TParent { public function __construct() { $this->foo(); } protected function foo() { echo "TParent#foo\n"; } } class TChild extends TParent { public function __construct() { parent::__construct(); } protected function foo() { parent::foo(); echo "TChild#foo\n"; } } new TChild(); ?>
class TParent def initialize foo end private def foo puts "TParent#foo" end end class TChild < TParent def initialize super end private def foo super puts "TChild#foo" end end TChild.new
解説
正解は「C++」。
C++以外の言語では期待した、
TParent#foo TChild#foo
という出力が得られますが、C++では単に「TParent#foo」とだけ出力されます。問題のコードはメソッドのオーバーライドの機能を使用して、以下のような順番で処理が進むことを期待しています。
- TChildクラスのインスタンスを生成
- TChildクラスのコンストラクタが呼び出される
- TParentクラスのコンストラクタが呼び出される
- TParentクラスのコンストラクタ内でfooメソッドを呼び出す
- TChildクラスのfooメソッドが呼び出される
- TChildクラスのfooメソッドがTParentクラスのfooメソッドを呼び出す
もちろん一般的には、コンストラクタの処理中は、派生クラスでオーバーライドされる可能性のあるメソッドを呼び出すべきできはありません。構築途中のインスタンスが外部に漏れる危険性があるからです。Delphi/Java/C#/PHP/Rubyでは、TParentクラスのコンストラクタの処理中にTChildクラスのfooメソッドが呼び出されてしまいます。
しかしC++では「コンストラクタの中から自身の仮想メソッドを呼び出してたとしても、派生クラスのメソッドではなく、自身のメソッドが呼び出される」という仕様になっています。このため、TChildクラスのfooメソッドは呼び出されることなく「TParent#foo」とだけ出力されたのです。
なお、Rubyに関しては特徴的な動作があります。問題のコードではTParentクラスのfooメソッドは「private」として修飾され、一見すると派生クラスTChildではオーバーライドされないように思えます。しかし実際にはオーバーライドが行われてしまいます。この点、特に注意が必要でしょう。