C# 使いが Kotlin を使ってみて「いいな」と思ったトコ

最近 Kotlin をよく使っているので、 C# とくらべて「お、これはイイな!」と思ったところを挙げてみました、今後足してく予定。

これ↓も似たような話かな。

データクラス

C#

public class AddressCard 
{
  public string Name { get; }
  public string Phone { get; }
  
  public AddressCard(string name, string phone)
  {
    this.Name = name;
    this.Phone = phone;
  }
}

Kotlin

data class AddressCard(val name:String, val phone:String)

fun main() {
   val card = AddressCard("name", "phone")
   val copied = card.copy(name = "hoge") // 指定したプロパティだけ値を変えて複製してくれる
}

圧倒的短さ! & copy メソッドなにこれすごい! Json のモデルクラスを作る時にはほんと便利。

型定義の省略

C#

public class AddressCard 
{
  public ObservableField<string> Name { get; } = new ObservableField<string>("");
  public IDictionary<string, string> Map { get; } = new Dictionary<string, string>();
}

Kotlin

class AddressCard {
  val name = ObservableField<String>("")
  val map = HashMap<String, String>() // これは悪手。public ならちゃんと map:Map<String, String> と基本抽象型にすべき
}

圧倒的短さ! これが型を後ろに書く言語の強みなのか???

コンストラクタ引数をメンバ変数に入れるやつ

C#

public class AddressCard 
{
  public readonly string _name;
  public readonly string _phone;
  
  public AddressCard(string name, string phone)
  {
    _name = name;
    _phone = phone;
  }

  public string ToFullName() = $"{_name}:{_phone}";
}

Kotlin

class AddressCard(
  private val name:String, 
  private val phone:String) {
  
  fun toFullName() = "${name}:{phone}"
}

代入の必要ナシ! そもそもプライマリコンストラクタの引数はそのままプロパティになる模様(val を付けない場合はイニシャライザ init {} の中でのみ参照可能な変数になるとのことです、コメントで教えていただきました)。

読み取り専用かどうか

C#

public class AddressCard 
{
  public string Name { get; } // 読み取り専用プロパティ
  public string Phone { get; set; } // 書き込み可プロパティ

  private readonly string _fullName; // 読み取り専用フィールド
  
  public AddressCard(string name, string phone)
  {
    this.Name = name;
    this.Phone = phone;
    _fullName = $"{_name}:{_phone}"
  }
}

Kotlin

class AddressCard {

  val name:String   // 読み取り専用プロパティ
  var phone:String  // 書き込み可プロパティ

  private val _fullName:String // 非公開読み取り専用プロパティ

  constructor(name:String, phone:String) { // 敢えてのコンストラクタ
    this.name = name
    this.phone = phone
    _fullName = "${name}:{phone}"
  }
}

いかなるケースでも valvar の使い分けだけで済むのがイイ!

クラスでも null を排除できる

C#

class HogeClass {  }
struct HogeStruct {  }

void Main()
{
   HogeClass hogeClass = null; // null にできる   
   HogeStruct hogeStruct = null; // null にできない
   HogeStruct? nullableHoge = null; // null にできる
   int? num = null; // null にできる
   
   // null条件/合体演算子
   Console.WriteLine(hogeClass?.ToString() ?? "empty");
   Console.WriteLine(num?.ToString() ?? "empty");
}

Kotlin

class HogeClass {  }

fun main()
{
   val hogeClass:HogeClass = null; // null にできない
   val nullableHoge:HogeClass? = null; // null にできる
   val num:Int? = null; // null にできる
   
   // null条件/合体演算子
   System.out.println(nullableHoge?.ToString() ?: "empty");
   System.out.println(num?.ToString() ?: "empty");
}

末端関数において、 null になる可能性を排除して実装できる安心感パない。 (C# でも構造体を多用すればできるけど、目的が違うし実質ムリ)

"使用しない引数" を明示できる

C#

button.Clicked += (_, __) =>  // _ は2度使えない
{
    var hoge1 = _.ToString();  // 変数名が _ なだけ
    var hoge2 = __.ToString(); // 変数名が __ なだけ
};

Kotlin

button.setOnHoverListener({ _, _ ->  // _ は何度でも使える!
    val hoge1 = _.toString() // エラー
    val hoge2 = _.toString() // エラー
    true
})

主にイベントリスナーで、ラムダ式の「引数を使わない」ことを示すために、 C# では「引数名を _ にする」という文化があります。 が、あくまで文化でしかないので、変数 _ が存在しているだけであり、引数が2個あったら両者に _ は使えないし、関数内で _ は変数として普通に使えてしまいます。

一方で Kotlin の _ は「使用しない変数名」として特別視されており?、複数の引数に割り当てられるし、関数内で _ を使うとエラーにしてくれます。

C# でラムダ式をネストせざるを得ないときに、まあまあな頻度で _ を複数回使いたいなーと感じることがあるので、Kotlin の _ は便利だなー、と思います。

逆に Kotlin の「これは個人的には好かん」ところ

デフォルトで public

API設計は、慎重派なんですよ。 気持ちよさにかまけて非公開とすべきAPIに private を付け忘れそう。

Smart cast

fun doWork(card:AddressCard?) {
  val name = card.name  // nullableだからエラー
  if (card != null) { // null をチェックすれば…
    val name2 = card.name  // 大丈夫だ、nullの可能性は無い
  }
}

まあ便利で使いもするんだけど、 card?.let{ } で代用できるし(これも Smart Cast だとコメントで教えてもらいました)。

C# でも Nullable<T> なら 「スコープ内は null でないことを保証できる」 Let 拡張関数が書けるので、参考までに載せときます。

public static R? Let<T, R>(this T? self, Func<T, R?> mapper) where T : struct where R : struct 
    => self.HasValue ? mapper.Invoke(self.Value) : (R?)null;

int? num = 3;
int ret1 = num?.Let((int nonNullNum) => nonNullNum * 2) ?? -1; // -> 6

num = null;
int ret2 = num?.Let((int nonNullNum) => nonNullNum * 2) ?? -1; // -> -1

Non-local return

fun upperJoin(name:String, zip:String) : String {
    val nameUpper = name.let { x->
        if (x == "") {
            return ""      // upperJoin を抜けちゃう!
//          return@let ""  // この let {} スコープを抜けるだけ
        }
        return x.toUpperCase()
    }

    val zipUpper = zip.let { it.toUpperCase() }
    return "$nameUpper:$zipUpper"
}

上のコードは name が空文字の場合、zip を無視して空文字を返してしまいます。 いつかハマりそうで、オドオドしてます。

Smart-cast も Non-local return も、自分が知ってる言語にない機構に拒否反応を示してるだけですね。慣れたらガンガン使いそうです。

C# にしかない機能や、これから新たに追加される機能もあると思いますが、あくまで自分が使用する範囲で感じたこと、ということで。

C# の方ももちろん進化は続いていて、

に代表されるように、モダンと呼ばれる仕様も次々と取り込まれていく雰囲気が好きです。