しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

Swiftのコードにシェルスクリプトでfinalをつける

こんばんは、final警察です👮

既存のコードにfinalをつけるのがめんどくさかったので、シェルスクリプトで一括finalをつけてみました。

Bashスクリプト

Bashわかんねーって言いながら、Mojaveまでデフォルトだった3.2でも動くように書いてみました。

#!/usr/bin/env bash

TARGET_DIR="Sources/"
while read result
do
  RESULT=(${result//:/ })
  FILE_PATH=${RESULT[0]}
  CLASS_NAME=${RESULT[2]}
  if [ `git grep -IE "class.+:.*[, ]$CLASS_NAME\W" $TARGET_DIR | wc -l` -eq 0 ]; then
    echo "Added 'final' to $CLASS_NAME."
    sed -i -E "s/^( *)class $CLASS_NAME/\1final class $CLASS_NAME/" $FILE_PATH
  fi
done < <(git grep -I "^ *class [A-Z]" $TARGET_DIR)

ちょっといじればJavaとかにも使えると思います。

解説

git grep

git管理下の場合、普通のgrepより速いです。
デフォルトで8スレッドで動いてくれます。 -Iでバイナリファイルを除いた検索ができます。

もっと速いgrepも探すとあります。

class.+:.*[, ]$CLASS_NAME\W

継承しているクラスを探します。 $CLASS_NAMEBaseの時に、

class Hoge: Base {

のような書き方にマッチします。
末尾の\WHoge2のような部分一致のケースを弾いています。

Lintが入っている前提の、ある意味性善説的なマッチなので、

class Hoge:Base {

のようなイレギュラーなケースに対応する場合は、class.+:(.*[, ]){0,1}Base\Wみたいに書いておいたほうが安全です。

あと、typealiasを使って継承みたいな書き方をされると、対応できません。

while read ~ done < <(expr)

forだとスペース入りの文字列がsplitされてしまうので、こう書いています。

なぜfinalをつけるか

保守性の文脈だと、Swiftでは基本的に継承は使わず、protocolを使うことになってるはずなので、不用意な継承を避けるためにもfinalをつけたほうが良いです。
その他の言語でも、大体コーディング規約などで推奨されているはず。(たとえば書籍だとEffective Java(3rd Edition)のItem 17とか)

Effective Java (English Edition)

Effective Java (English Edition)

とくにSwiftの場合、C++などと同じく、継承される可能性がなくなれば、実行時に実際に呼ばれるメソッドやプロパティを決めるDynamic Dispatchの必要がなくなるため、パフォーマンスが上がります。

developer.apple.com

Dynamic Dispatchの話はまた今度。

締め

厳密にやるなら、がっつりSwift Syntaxとかでやる感じでしょうか。

仕事しんどいし、腰の左あたりがめっちゃいたいので、しんどくなくなって忘れてなかったらやります。