C#による開発経験しかなかった私が急遽Flutter/Dartを学ぶ必要性が出てきたため、Dartの言語仕様をC#と比較しながら簡単にまとめてみました。
細かい仕様については誤りがあるかもしれないのでその点についてはご容赦ください。
ボリュームが多いので記事を全3回に分けました。
言語のバージョン
- Dart 3.4.3
- C# 12 (.NET 8)
変数
変数の宣言方法は基本的にC#と同じです。初期値の指定は任意で型推論があります。
// 初期値なし
int a;
// 初期値あり
int b = 0;
// 型推論
var c = 0;
定数
定数は2通りの宣言方法があり、Dartではプログラム実行時に値が設定されるfinal
と、コンパイル時に値が設定されるconst
があります。いずれも型推論による宣言が可能です。
// 実行時定数
final int a = 123;
final b = 123;
// コンパイル時定数
const int c = 456;
const d = 456;
C#ではconst
は同じですが、final
の代わりにreadonly
を使います。(readonly
はクラス内のフィールドでしか使えないといった制限あり)。型推論による宣言はできません。
// 実行時定数(クラスのフィールドでのみ使用できる)
readonly int a = 123;
// コンパイル時定数
const int b = 456;
型
数値型
Dartでは数値型はint
(整数型)とdouble
(浮動小数点型)の2つがあります。
int a = 1;
double b = 1.1;
C#ではintやdoubleのほかに様々な組み込み型が用意されています。以下によく使われる型の例を記載します。
// 整数型(符号あり)
int i = 1;
long l = 1L;
// 浮動小数点型
float f = 1.1f;
double d = 1.1;
// 10進小数
decimal = 1.1m;
文字列型
文字列型はC#と基本的には同じですが、DartではStringが大文字だったり、シングルクォートも使える点が異なります。
// シングルクォートでもダブルクォートでも可
String str1 = 'Hello!';
String str2 = "Hello!";
// 文字列連結(+演算子が無くても連結される)
String str3 = 'Hello,' 'World!' // => Hello,World!
String str4 = 'Hello,' + 'World!' // => Hello,World!
// 文字列補間
String name = 'Taro'
String str5 = 'Hello, ${name}' // => Hello,Taro!
// 生文字列(特殊文字の表示)
String str6 = r'Hello,"World!"' // => Hello,"World!"
以下はC#の記法です。
// ダブルクォートのみ
string str1 = "Hello!";
// 文字列連結
string str2 = "Hello," + "World!" // => Hello,World!
// 文字列補間
string name = "Taro"
string str3 = "Hello, ${name}" // => Hello,Taro!
// 逐語的文字列リテラル
string str4 = @"Hello,""World!""" // => Hello,"World!"
// 生文字列リテラル(C#11以降)
string str5 = """Hello,"World!"""" // => Hello,"World!"
C#での補足として、特殊文字をエスケープせずにそのまま出力したい場合は逐語的文字列リテラル(文字列の前に@
を付ける)を使いますが、ダブルクォートは連続して記述する必要がありました。
C#11以降は生文字列リテラル("""
で文字列を囲む)を使えば、ダブルクォートも1つの記述で問題なくなります。
論理型
論理型はDartもC#も同じです。
bool flag1 = true;
bool flag2 = false;
List
DartでもC#でもListクラスが用意されており、同様の使い方ができます。
用意されているプロパティやメソッドも似ているので、若干名前が違ったりはしますが基本的には同じことができると思っていてよさそうです。
まずはDartでのListの宣言方法です。
List<int> list1 = [1, 2, 3,]; // 末尾にカンマもOK
final list2 = <int>[1, 2, 3]; // 型注釈
C#でのListの宣言方法です。C# 12以降はDartとほぼ同じ書き方ができるようになりました。
List<int> list1 = new() { 1, 2, 3 };
var list2 = new List<int> { 1, 2, 3 };
var list3 = [1, 2, 3]; // コレクション式(C# 12以降)
Listのメソッドやプロパティは数が多いので、よく使うものを比較表にしてみました。(C#ではLINQも含む)
用途 | Dart | C# |
---|---|---|
要素を追加 | list.add(4) | list.Add(4) |
複数の要素を追加 | list.addAll([4, 5, 6]) | list.AddRange([4, 5, 6]) |
条件を満たす最初の要素を削除 | list.remove(2); | list.Remove(2); |
条件を満たす全ての要素を削除 | list.removeWhere(x => x > 2) | list.RemoveAll(x => x > 2) |
全要素の削除 | list.clear() | list.Clear() |
要素の数を取得 | list.length | list.Count |
リストが空か | list.isEmpty | list.Any() or list.Count > 0 |
リストが空でないか | list.isNotEmpty | !list.Any() or list.Count != 0 |
特定の要素が含まれるか | list.contains(2) |
|
最初の要素を取得 | list.first | list.First() |
条件に一致する要素があるか | list.any(x => x % 2 == 0) | list.Any(x => x % 2 == 0) |
すべての要素が条件を満たすか | list.every(x => x % 2 == 0) | list.All(x => x % 2 == 0) |
条件を満たす最初の要素を取得 | list.firstWhere(x => x ==1) | list.First(x => x ==1) |
条件を満たす最初の要素を取得(要素が無い場合はデフォルト値) | list.firstWhere(x => x ==1, orElse: () => 0) | list.FirstOrDefault(x => x ==1) |
条件を満たす複数の要素を取得 | list.where(x => x % 2 == 0) | list.Where(x => x % 2 == 0) |
全ての要素を変換 | list.map(x => x * 2) | list.Select(x => x * 2) |
要素の合計値を計算 | list.reduce((a, b) => a + b) | list.Sum() |
Map(Dictionary)
いわゆる連想配列のことで、DartではMapクラス、C#ではDictionaryクラスを使用します。
どちらも使い方はほぼ同じですが、初期化方法に若干の違いがあります。
// 初期化
var map1 = { 'Japan' : 'Tokyo', 'France' : 'Paris', 'Spain' : 'madrid' };
var map2 = <String, String>{ 'Japan' : 'Tokyo', 'France' : 'Paris', 'Spain' : 'madrid' }; // 型注釈
// 要素の取得
var tokyo = map1['Japan'];
// 要素の追加
map1['USA'] = 'Washington, D.C.';
// キー・要素の存在確認
map1.containsKey('Japan'); // -> true
map1.containsValue('Japan'); // -> false
// 要素の削除
map1.remove('Japan'); // -> Tokyo
C#では初期化以外はDartと同じような書き方ができます。
// 初期化
var dict1 = new Dictionary<string, string> { { "Japan", "Tokyo" }, { "France", "Paris" }, { "Spain", "madrid" }, };
var dict2 = new Dictionary<string, string> { ["Japan"] = "Tokyo", ["France"] = "Paris", ["Spain"] = "madrid", };
// 要素の取得
var tokyo = dict1["Japan"];
// 要素の追加
dict1["USA"] = "Washington, D.C."; // 要素の上書きも可能
dict1.Add("China", "Beijing");
// キー・要素の存在確認
dict1.ContainsKey("Japan"); // -> true
dict1.ContainsValue("Japan"); // -> false
// 要素の削除
dict1.Remove("Japan"); // -> true
Record型(タプル型)
Record型とはいわゆるタプル型のことで、Dartではバージョン3で導入されました。
C#とほぼ同様の使い方ができます。
// 様々な初期化方法
var record1 = ('Taro', 30);
(String, int) record2 = ('Taro', 30); // 型注釈
var record3 = (name: 'Taro', age: 30); // 名前付きフィールド
(String name, int age) record4 = ('Taro', 30); // 名前付きの型注釈
var tuple1 = ("Taro", 30);
(string, int) tuple2 = ("Taro", 30); // 匿名タプル
var tuple3 = (name: "Taro", age: 30); // 名前付き
(string name, int age) = ("Taro", 30); // タプルの分解
以下のように、メソッドの戻り値として複数の値を返したい場合によく使われます。
(String, int) getUser() {
return ('John', 23);
}
ジェネリクス
ジェネリクスとは様々な型に対応するための仕組みで、Dartでも標準で用意されています。ListやMapもジェネリクスの仕組みが使われています。
ジェネリッククラス
クラス名の後ろに<型パラメータ名>
を記載します。パラメータ名はC#と同様にT
という名前を使うのが一般的です。
// ジェネリッククラス
class Sample<T> {
// T型のフィールド
T _value;
// コンストラクタ
Sample(this._value);
// T型の戻り値を返すメソッド
T getValue() {
return _value;
}
}
void main() {
// 様々な型でSampleクラスのインスタンスを生成
final intSample = Sample(3);
final stringSample = Sample('hello');
// SampleクラスのgetValueメソッドを実行
print(intSample.getValue()); // => 3
print(stringSample.getValue()); // => 'hello'
}
同様の内容をC#で書き直したのが以下です。newする際に型指定が必要といった制約はありますが、同様の書き方が可能です。
internal class Sample<T>
{
private T _value;
internal Sample(T value)
{
_value = value;
}
internal T GetValue()
{
return _value;
}
}
internal class Program
{
static void Main()
{
var intSample = new Sample<int>(3);
var stringSample = new Sample<string>("hello");
Console.WriteLine(intSample.GetValue()); // => 3
Console.WriteLine(stringSample.GetValue()); // => 'hello'
}
}
ジェネリックメソッド
ジェネリックメソッドではメソッド名の後ろに型パラメータ名を指定します。
以下のサンプルコードは、任意の型のlistを受け取り、listが空だったらnullを、そうでない場合は最初の値を返すジェネリックメソッドです。
// Listの最初の値を返すジェネリックメソッド
T? getFirstValue<T>(List<T> list){
if (list.isEmpty) {
return null;
}
return list.first;
}
void main() {
print(getFirstValue([1, 2, 3])); // => 1
print(getFirstValue(['a', 'b', 'c'])); // => a
print(getFirstValue([])); // => null
}
C#で書き直すと以下のようになります。
internal class Program
{
static T? GetFirstValue<T>(List<T> list)
{
if (list.Count() == 0) return default;
return list.First();
}
static void Main()
{
Console.WriteLine(GetFirstValue([1, 2, 3])); // => 1
Console.WriteLine(GetFirstValue(["a", "b", "c"])); // => a'
Console.WriteLine(GetFirstValue(new List<string>())); // => (何も表示されない)
}
}
型制約
ジェネリックメソッドなどでは受け取る型に対して制約を付けることも可能です。
Dartの場合は型パラメータの後ろにextends 特定のクラスやインターフェース名
と記述することで、特定のクラスやインターフェースを継承した型のみを受け取れるように制限できます。
T calc<T extends num>(T value) // Tは数値型(intかdouble)
T calc<T extends SampleClass>(T value) // TはSampleClassを継承したクラス
C#ではwhere
句の後に制約内容を記述します。
T Calc<T>(T value) where T : INumber<T> // Tは数値型(C#11以降で使用可)
T Calc<T>(T value) where T : ISample // TはISampleインターフェースを実装したクラス
続きの記事
おすすめ書籍
Flutterを使ったアプリ開発を実践的に学べる入門書です。Dart言語の仕様も一通り紹介されているので初めてFlutter開発にチャレンジする方にもおすすめです。
タイトルの通り、実務で役に立つイディオムやパターンが豊富に紹介されており、初級者から中級者まで幅広くおすすめできる書籍です。(2024年7月に改訂版が発売しました)