Swiftにおけるfinal修飾子とその強制
はじめに
Swiftには他の言語と同じようにfinal修飾子があります.
この修飾子はLanguage Guideに次のような説明がされています.
You can prevent a method, property, or subscript from being overridden by marking it as final. Do this by writing the final modifier before the method, property, or subscript’s introducer keyword (such as final var, final func, final class func, and final subscript).
Any attempt to override a final method, property, or subscript in a subclass is reported as a compile-time error. Methods, properties, or subscripts that you add to a class in an extension can also be marked as final within the extension’s definition.
You can mark an entire class as final by writing the final modifier before the class keyword in its class definition (final class). Any attempt to subclass a final class is reported as a compile-time error.
要約すると,この修飾子を用いることにより,プロパティやメソッドのオーバーライドおよびクラスの継承を禁止することができます.
継承を禁止する2つの利点
さて,このfinal修飾子を使う利点とはなんでしょうか.
私は大きく分けて次の2つが挙げられると考えています.
- パフォーマンスの向上
- 変更の影響範囲を限定する
パフォーマンスの向上
これは,Appleの公開しているブログにも言及されています.
The final keyword is a restriction on a class, method, or property that indicates that the declaration cannot be overridden. This allows the compiler to safely elide dynamic dispatch indirection. For instance, in the following point and velocity will be accessed directly through a load from the object’s stored property and updatePoint() will be called via a direct function call. On the other hand, update() will still be called via dynamic dispatch, allowing for subclasses to override update() with customized functionality.
final修飾子を使用してあるクラスの継承を禁止すると,そのクラスのプロパティやメソッドをDynamic Dispatchを介さずに直接読み込めるようです.
その結果,パフォーマンスが向上すると述べられています.
より具体的に考えてみましょう,
例えば,次のように,AnimalクラスとCatクラスがあるとします.
Animalクラスは継承可能となっているため,animal.eat()
にて実際に呼び出されるのはAnimalクラスかそのサブタイプ(Catクラス)のeat()
となります.
よって,コンパイル時に実際に何が呼ばれるのかを判定する処理(*1)が必要になります.
class Animal {
func eat() {
print("eat")
}
}
class Cat: Animal {
override func eat() {
super.eat()
print("cat food")
}
}
let animal: Animal = Cat()
animal.eat()
しかし,Animalクラスにfinal修飾子が付いていたらどうでしょうか.
その場合はanimal.eat()
で呼び出されるのはAnimalクラスのeat()
になることが約束されます.
よって,コンパイル時に直接Animalクラスの`eat()を直接アクセスすれば良くなるため,パフォーマンスが向上します.
変更の影響範囲を限定する
final修飾子を使用することは,そのクラスの変更による影響を最小限にすることに繋がります.
結果的に,意図せずバグを生んでしまったり,コードの可読性を向上させることに寄与します.
先述したCatとAnimalを例にとります.
CatはAnimalを継承し,そのメソッドをオーバーライドし,その中でAnimalのメソッドを呼び出しています.
そのため,Animalのメソッドに変更を加えた場合,Catのメソッドの振る舞いが変わります.
つまり,これはAnimalに変更を加える度に,そのサブタイプが破壊されていないか確認する必要があることを示しています.
こういった,不用意に受ける影響の確認と修正を要されるくらいなら,継承させる必要がないクラスにはfinal修飾子を付けた方が良いかもしれません.
final修飾子を強制する
先述したようなfinal修飾子による恩恵を最大限受けるために,継承されていないクラスにはfinal修飾子を付けましょう.
しかし,いくら意識したところで,プログラミング中のトライ&エラーの過程でfinal修飾子のことをすっかり忘れてしまうかもしれません.
もしくは,チームにそういった意識が根付いていないかもしれません.
どうにか,機械的に解決(final修飾子を強制)できないでしょうか?
この方法には次の2つがあると思います.
- 静的解析ツールを使用して,継承していないクラスを指摘する.
- 静的解析ツールやフォーマッターを活用して,継承されていないクラスに対して自動的にfinal修飾子を付ける
今回はDangerを用いて,前者の解決方法を試みました.
Dangerプラグインの作成
継承されていないクラスを検知し,PRのコメントにてそれを指摘するDangerプラグインを作成しました.
なお,このプラグインが検知できるのは次のケースです.
- 既にあるrclassへ付けてあったfinalが誤って外された時
- 新しく定義したタイプにfinalが必要な時
- 消去したタイプの親タイプにfinalが必要な時
普段rubyを書き慣れていないため,読みにくいのはご容赦下さい🙏🙏
(また,何か例外があればご指摘頂けると嬉しいです)
module Danger
class ForceSwift < Plugin
Class = Struct.new(:parent_type, :self_type, :is_final?)
LinePosition = Struct.new(:file_path, :line_number)
def warn_if_needed(target_paths)
all_classes = []
modified_classes = []
deleted_classes = []
@position = {}
diff_swift_files = (git.modified_files + git.added_files)
.filter { |file| File.extname(file) == '.swift' }
.map { |file| git.diff_for_file(file).patch }
project_files = target_paths
.flat_map { |root| Dir[File.join("#{root}/**", '*.swift')] }
diff_swift_files.each do |text|
text.split("\n").each do |line|
case line
when /^\+/
add_class_to_list(line, modified_classes)
when /^\-/
add_class_to_list(line, deleted_classes)
end
end
end
project_files.each do |path|
line_number = 0
File.open(path) do |file|
file.each_line do |line|
line_number += 1
is_success = add_class_to_list(line, all_classes)
@position[all_classes.last.self_type] = LinePosition.new(path, line_number) if is_success
end
end
end
modified_classes
.each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
deleted_classes
.flat_map { |a_class| parents(a_class, all_classes) }
.each { |a_class| warn_if_final_is_needed(a_class, all_classes) }
end
private
def children(reference_class, all_classes)
all_classes.filter { |a_class| a_class.parent_type.include?(reference_class.self_type) }
end
def parents(reference_class, all_classes)
all_classes.filter { |a_class| reference_class.parent_type.include?(a_class.self_type) }
end
def warn_if_final_is_needed(a_class, all_classes)
children = children(a_class, all_classes)
is_leaf = children.empty?
if is_leaf && !a_class.is_final?
warn(
"おそらく#{a_class.self_type}にはfinal修飾子が必要です.",
file: @position[a_class.self_type].file_path,
line: @position[a_class.self_type].line_number
)
else
children.each { |node| warn_if_final_is_needed(node, all_classes) }
end
end
def add_class_to_list(line, list)
class_difinication_match = line.match(/class (.+) {/)
return false unless !class_difinication_match.nil? && !class_difinication_match[1].nil?
is_final = !line.match(/.*final.+class/).nil?
hierarchy_match = class_difinication_match[1].match(/(.+): (.+)/)
if !hierarchy_match.nil? && !hierarchy_match[2].nil?
parents = hierarchy_match[2].split(',').map { |line| line.strip }
list << Class.new(parents, hierarchy_match[1], is_final)
else
list << Class.new([], class_difinication_match[1], is_final)
end
true
end
end
end
このプラグインを使用すると次のように,Botが指摘してくれます.
まとめ
- final修飾子を付けることには2つのメリットがある
- 付けるのを忘れがちならDangerプラグインを使うなどの解決方法がある
注釈
(*1) たぶんこれがDynamic Dispatch
参考文献
- the swift programming language https://docs.swift.org/swift-book/LanguageGuide/Inheritance.html
- Increasing Performance by Reducing Dynamic Dispatch https://developer.apple.com/swift/blog/?id=27
- DangerでPRのコード差分にインラインでコメントをつける https://qiita.com/rnishimu22001/items/a76ee62f93a78460000d
- SwiftにおけるMethod Dispatchについて https://qiita.com/HaNoHito/items/f40bfc1717c1e922a5b0#dynamic-dispatch
Discussion
class funcが考慮から漏れた実行になっていた😂
まとめ
private: プロパティ、メソッドに付与することでOverride宣言を探す範囲を現在のファイルに限定できる。つまりコンパイル時間が短縮化される。
final: finalが無いクラスのメソッドは直接呼出しコードに変換されない。メソッドテーブルから関数を探索する間接呼出しコードに変換され実行時にパフォーマンスの低下を招く。
疑問
finalが無いクラスをコンパイルする際、Override宣言が無い場合に自動的にメソッド呼出しを直接呼出しコードに変換してくれないのだろうか?