様々な値の型について知ろう 【プログラミング発展】
どの言語にも、値には型が存在します。型とは、値がどのような形式か、文字列なのか、整数なのかなどを決めるものです。ここでは、Javascript以外の言語も含めた型を紹介します。
目次
代表的な型
ユーザが作成するクラスを除くと、これらの型が代表的です。
- 整数
- 実数
- 多倍長整数
- 真偽値
- 文字列
- 参照
- 配列
- 関数
- その他、各クラス (Exceptionやデータ構造を抽象化したクラスなど)
どの言語でも全てあるというわけではありません。C言語では文字列という型はなく、char
の配列またはポインタになります。
それぞれの型
プリミティブ型
いわゆる基本的な型です。特徴は、変数に格納するときは、値の本体そのものが格納されるということです。
数値系、文字列、真偽値、参照やnullなどがこれに当たります。
それ以外の型は言語によって扱いが変わります。Javascriptであればオブジェクト型がそれに当たります。JavaやJavascript, Pythonなどでは、オブジェクト代入時に配列本体ではなくその参照の値が代入されるため、プリミティブ型かどうかを意識する必要があります。
つまり、プリミティブ型かそうでないかを知るのは重要です。
関数
関数も型の一つです。特徴は()
を付けるとその関数の中身を実行できることですね。
また、変数に代入することもできます。
つまり、関数は特別な存在ではなく、関数という型の値であるということです。値ということは、数値や配列のように、変数に入れたり、戻り値にしたりといったことが可能ということです。
配列
特にC言語などでは、メモリが連続して確保されるという特徴があります。
他の言語では言語によりますが、代入時は本体ではなく参照がコピーされる、というのはよくあることです。
データ構造の面だと、配列とリストはしばしば明確に区別されます。配列は連続したメモリ領域を確保しますが、リストはグラフ構造 (数珠つなぎになっている) であり、連続したメモリを確保するわけではありません。
この違いは、配列の途中に要素を入れる、削除する、読み取るといった処理を行う際に、明確な速度差を生みます。
参照
参照型は、オブジェクトの場所を入れる型です。イメージはC言語のポインタとほぼ同じです。
多くの言語では、クラスのオブジェクト、配列、連想配列などは、代入する際に参照が代入されます。
代入後に中身を変更すると、他の代入先にも影響が出る場合があるのは、本体をコピーしているのではなく、参照をコピーして代入しているためです。
自動的な変換 (動的型付け言語の場合)
Javascriptなどの動的型付け言語では、自動的に型が変換されることがあります。
const a = 100; const s = "foo"; console.log(a + s);
100foo
数値と文字列を足し算しています。文字列同士は文字の連結ができますが、数値と文字列の場合、数値が文字列に変換された上で連結されます。
C++など、一部の言語ではoperatorをオーバーロードすることで似たようなことが可能です。
そのため、以下のようなパターンに注意が必要です。
const a = 100 const b = 200; const s = "foo"; console.log(a + b + s);
300foo
まずa + b
が普通に足し算され、そのあとs
と文字列として連結しています。もしかしたら、a
とb
は数値で足し算ではなく、文字列として連結したかった可能性があります。その場合は、文字列に変換しなければなりません。
const a = 100 const b = 200; const s = "foo"; console.log(a.toString() + b.toString() + s);
100200foo
動的型付け言語は、表面的には型を意識する必要性が低いと思われがちです。しかし、実際に扱うにあたっては自動的な型変換、エディタ側の型推論の失敗、代入、条件式などでの思わぬ型の変化があるため、静的型付け言語よりも慎重に型を扱う必要性があります。
静的型付け言語での自動的な型変換
大きく2つのパターンが考えられます。
- 整数から実数への変換、符号なしから符号ありの変換、あるいはそれらの反対や、バイト長が短くなるような実数や数値の変換
- operatorをオーバーロードすることでの、見かけ上の自動的な型変換
前者は値の一部が切り捨てられるため、エディタがしばしば警告を出しますね。
int a = 5 + 1.3; // '初期化中': 'double' から 'int' への変換です。データが失われる可能性があります。
後者は一部の言語で可能です。あくまで見かけ上なので、型に厳密であることは変わらず、やはり安全と言えます。
#include <iostream> #include <string> class MyString { public: MyString() {}; MyString(const std::string& s) : s(s) {}; virtual ~MyString() {}; // +演算子をオーバーロード std::string&& operator+ (int rhs) const { return s + std::to_string(rhs); } private: std::string s = ""; }; // friendは省略 int main() { MyString ms("hoge"); std::string&& s = ms + 100; std::cout << s << std::endl; }
hoge100
データの持ち方
なぜプリミティブ型かそうでないかが大事なのかは、データの持ち方に差があるためです。
プリミティブ型は、変数に入っている値がそれそのものであることが一般的です。
それ以外の型は本体が別のところにあり、変数に入っているのはその参照である、というのが一般的です。
C++など、例外はあります。
次のコードは、実際にその差が確認できる例です。
const a = 10; let b = a; b = 100; console.log(a); console.log(b);
10 100
プリミティブ型は普通です。オブジェクトの場合は異なります。
const a = [1, 2, 3]; const b = a; b[1] = 100; console.log(a); console.log(b);
[ 1, 100, 3 ] [ 1, 100, 3 ]
b[1]
への代入ですが、a
の値も変わっています。これは、a
が持っているのは値の本体ではなく、あくまで参照の値であり、代入時にはその参照の値がコピーされるためです。
ようは変数は配列本体を持っていないために本体はコピーされておらず、複数の変数が同じ本体を共有しているため、このような現象が起きます。
この挙動を避けたい場合、いわゆるディープコピーが必要になります。詳しくは各言語の仕様を確認してください。
まとめ
多くの言語に共通する型に関して紹介しました。
言語による挙動の違い、演算子による自動的な型変換など注意点があるので、とくに動的型付け言語では型の取り扱いには注意しましょう。
