Swift4.0とSwift3.0-違いと新機能
公開: 2021-10-05Appleが有名なSwift言語の新しいバージョンをリリースした日がついに私たちを襲った。 私たちはそれから何を期待すべきですか? 見越しての震えながら、我々はスウィフト4バージョンに存在する新鮮なアップデートの小さな概要を提示することにしました。
3スウィフトの基礎。
コードを書くための素晴らしい言語として、Swiftには独自の利点があり、Objective-C言語よりも「長生き」すると言われています。
SwiftとObjective-Cの主な違いについてお読みください。
Swiftは高速で、タイプセーフで、非常に表現力豊かです。 これは、電話やタブレット、デスクトップ、サーバーでソフトウェアを作成するために使用できます。明らかに、コードを実行するすべてのもので使用できます。 AppleのSwiftPlaygroundsアプリのコーディング方法を学ぶか、XcodeでPlaygroundsを使用すると、作業の結果をすぐに確認できます。アプリの開発と実行に頭を悩ませる必要はありません。最初の場所。 新しい添加剤バージョンごとに、それはより良く、より速くなります、そしてそれはSwift4バージョンの場合です。
準備はできていますか?
Xcode9がSwift4用に備えているもう1つの優れた機能です。今後の移行についてあまり心配する必要はなく、この記事を読んでいるうちにその理由がわかります。
そういえば、今秋にボンボンとSwift4の新機能がもたらすものを簡単に見てみましょう。
入門
言語自体は、開発者の世界ではXcodeである便利なIDEなしではあまり役に立ちません。 Xcode 9の最新バージョンは、Mac AppStoreまたはAppleDeveloperサイトのダウンロードページからダウンロードできますが、最初にアクティブな開発者アカウントを持っていることを確認してください。 かなり安定しているので、毎日のコーディングルーチンでXcode8を置き換えることができます。
xcversionを使用してXcodeの複数のバージョンをインストールすることもできます
あなたが新しいプロジェクトを始めているなら-あなたは行ってもいいです。 ただし、Swift 3.xで作成されたプロジェクトがすでにある場合は、移行プロセスを実行する必要があります。
新しい機能の使用に慣れるために、最初にPlaygroundを試してみることをお勧めします。
この記事を読んでいると、 「SE -____」形式のSwiftEvolutionプロポーザルへのリンクに気付くでしょう。
Swift4への移行
Swiftのメジャーバージョンから次のバージョンへの移行は、特にSwift 2.xから3.0への移行では、常にかなり激しいものでした。 通常、プロジェクトごとに約1〜2日かかりますが、Swift 4への移行は少し簡単で、はるかに高速に渡すことができます。
移行前の準備
Xcode9はSwift4だけでなく、移行バージョン3.2もサポートしているため、プロジェクトは厳しい問題なくコンパイルできます。 これが可能なのは、Swift4コンパイラと移行ツールが両方のバージョンの言語をサポートしているためです。 ターゲットごとに異なるSwiftバージョンを指定できます。これは、一部のサードパーティライブラリがまだ更新されていない場合、またはプロジェクトに複数のターゲットがある場合に非常に役立ちます。 ただし、言語だけでなく、SDKにもいくつかの変更が加えられているため、AppleがSDK APIを使い果たしているため、コードにいくつかの更新を適用する必要がある可能性が非常に高くなります...
迅速な移行ツール
いつものように、AppleはXcodeにバンドルされたSwift移行ツールを提供しており、以前のSwiftバージョンからの移行に役立ちます。 Xcodeで起動するには、[編集] -> [変換] -> [現在のSwift構文に…]に移動し、変換するターゲットを選択します。
次に、どのObjective-C推論設定を適用するかを尋ねられます。
Swift 4では追加の変更が支配的であるため、Swift移行ツールがほとんどの変更を管理します。
CocoaPods
Swift Package Managerは非常に急速に改善されていますが、それほど成熟していないため、ほとんどの開発者はプロジェクトにCocoaPods依存関係マネージャーを使用しています。 上記のように、すべてのサードパーティライブラリがまだSwift 4に更新されているわけではないため、一部のライブラリのコンパイル中にエラーが発生する可能性があります。 この問題を修正するための1つの可能な解決策は、 post_install
スクリプトをpost_install
追加することにより、まだ更新されていないポッドにSwiftバージョン3.2
を指定することPodfile
。
old_swift_3_pods = [ 'PodName1', 'PodName2', ] post_install do |installer| installer.pods_project.targets.each do |target| if old_swift_3_pods.include? target.name target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '3.2' end end end end
次に実行します
pod install
これで、エラーなしでポッドをコンパイルできます。
Swift 4APIの変更と追加について調べてみましょう。
APIの変更と追加
文字列
SE-0163の提案により、 String
はCollection
プロトコルに準拠するようになりました。 Swift 1.xを覚えていますか?
String
直接反復できるため、 characters
配列プロパティは必要ありません。
let string = "Hello, Mind Studios!" for character in string { print(character) }
これは、 count
、 isEmpty
、 map()
、 filter()
、 index(of:)
などの任意のCollection
メソッドとプロパティをString
で使用できることも意味します。
string.count // No more `string.characters.count` string.isEmpty // false let index = string.index(of: " ") // 6 let reversedCollection = "abc".reversed() let reversedString = String(reversedCollection) // "cba" // String filtering let string = "ni123n456iniASijasod! 78a9-kasd aosd0" let numbersString = string.filter { Int(String($0)) != nil } // "1234567890"
新しいSubstring
タイプ
Swift 4は、 String
サブシーケンスを表す新しいSubstring
タイプをもたらします(上記のSE-0163で説明されています)。
// Split string into substrings let string = "Hello, Mind Studios!" let parts = string.split(separator: " ") // ["Hello,", "Mind", "Studios!"] type(of: parts.first!) // Substring.Type
どちらのString
とSubstring
、新しいサポートStringProtocol
彼らはほとんど同じと相互運用が可能になり:
var hello = parts.first! // Concatenate a String onto a Substring hello += " !" // "Hello, !" // Create a String from a Substring let helloDog = String(hello) // "Hello, !"
重要な注意点
SE-0163には非常に重要な注意事項があります。
Long-term storage of `Substring` instances is discouraged. A substring holds a reference to the entire storage of a larger string, not just to the portion it presents, even after the original string's lifetime ends. Long-term storage of a substring may therefore prolong the lifetime of elements that are no longer otherwise accessible, which can appear to be memory leakage.
つまり、 Substring
は、 String
サブシーケンスの一時ストレージとして使用することを目的としています。 それをいくつかのメソッドまたは他のクラスに渡したい場合は、最初にString
に変換します。
let substring: Substring = ... // Substring let string = String(substring) // String someMethod(string)
とにかく、スウィフトの型システムは、合格していないあなたを助けるSubstring
どこかにString
(あなたが新しい使用していないと仮定して期待されているStringProtocol
パラメータの型としての)。
複数行の文字列リテラル
SE-0168では、3つの二重引用符"""
を使用した複数行の文字列リテラルの簡単な構文が導入されています"""
これは、ほとんどのテキスト形式(JSONやHTMLなど)または一部の長いテキストをエスケープせずに貼り付けることができることを意味します。
let multilineString = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mattis lorem et leo laoreet fermentum. Mauris pretium enim ac mi tempor viverra et fermentum nisl. Sed diam nibh, posuere non lectus at, ornare bibendum erat. Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque. Vestibulum vulputate augue ac libero vulputate vestibulum. Nullam blandit et sapien non fermentum. Proin mollis nisl at vulputate euismod. """
文字列リテラルでの改行のエスケープ
SE-0182は、行末に円記号を付けて、複数行の文字列リテラルの改行をエスケープする機能を追加します。
let escapedNewline = """ Line 1, Line 2 \ next part of line 2, Line 3 """ print(escapedNewline)
Line 1, Line 2 next part of line 2, Line 3
Unicodeサポートの改善
Swift4はUnicode9のサポートをもたらします。これは、Unicode文字のカウントに関する問題がなくなったことを意味します。
"".count // 1, in Swift 3: 2 "".count // 1, in Swift 3: 2 "".count // 1, in Swift 3: 2 - person + skin tone "".count // 1, in Swift 3: 4 "".count // 3, in Swift 3: 1
上で強調したすべての変更と実装された提案(他の多くの提案と同様)は、文字列マニフェストと呼ばれる広く説明されている一連の機能に端を発しています。
アクセス制御
Swift 3は、アクセス制御に非常に矛盾した要素をもたらしました。これは、非常に混乱する可能性のあるfileprivate
アクセス修飾子です。
以前は、 private
アクセスレベル修飾子を使用してタイプメンバーを他のタイプから非表示にし、プライベートメンバーは、タイプ定義で定義されたメソッドとプロパティによってのみアクセスでき、それらのメンバーにアクセスできなかったため、同じタイプ拡張を残していました。
fileprivate
を使用して、同じファイル内でプロパティやメソッドなどのタイプメンバーのアクセスを共有できます。
実際、 private
使用すると、あるタイプの拡張機能がそのタイプのメンバーにアクセスできない場合に問題が発生しました。そのため、このような状況でfileprivate
を使用することは非常に一般的な解決策であり、別の問題が発生しました。同じファイルがそれらのメンバーにもアクセスできます。
Swift 4は、SE-0169で説明されているように、タイプの拡張機能が同じファイル内のそのタイプのprivate
メンバーにアクセスできるようにすることで、物事を整理します。
struct User { private let firstName: String private let lastName: String } extension User: CustomStringConvertible { var description: String { return "User: \(firstName) \(lastName)" } }
辞書とセット
Sequence
Dictionary
初期化
Dictionary
はSequence
で初期化できますが、このイニシャライザですべてのシーケンスを渡すことはできません。タプル(Key, Value)
を含むシーケンスのみです。ここで、 Key
はディクショナリのキータイプであり、 Value
はディクショナリの値タイプを表します。
let stocksIdentifiers = ["AAPL", "GOOGL", "NKE"] let stocksValues = [158.28, 940.13, 53.73] let pairs = zip(stocksIdentifiers, stocksValues) let stocksValuesDict = Dictionary(uniqueKeysWithValues: pairs) // ["GOOGL": 940.13, "NKE": 53.73, "AAPL": 158.28]
ここで、 zip
関数は2つのシーケンスからペア( Tuple
)を作成します。この関数の詳細については、Swift標準ライブラリのドキュメントを参照してください。
辞書のマージ
2つの同一のキーの値を組み合わせるために使用されるuniquingKeysWith
パラメーターにクロージャーを渡すことにより、 Tuple
のシーケンスからディクショナリを作成するときに重複キーを処理する方法を指定できます。
例1:
let duplicates = [("a", 1), ("b", 5), ("a", 3), ("b", 3)] let dictionary = Dictionary(duplicates, uniquingKeysWith: { (first, _) in return first }) // ["b": 5, "a": 1]
ここでは、同じキーを持つ次のすべての値を無視して、最初の値を残します。
例2:
各文字が文字列に出現する回数を数えます。
let string = "Hello!" let pairs = Array(zip(string, repeatElement(1, count: string.count))) let counts = Dictionary(pairs, uniquingKeysWith: +) // ["H": 1, "e": 1, "o": 1, "l": 2, "!": 1]
例3:
merge
メソッドの使用:
let values = ["a": 1, "b": 5] var additionalValues = ["b": 3, "c": 2, "a": 3] additionalValues.merge(values, uniquingKeysWith: +) // ["b": 8, "c": 2, "a": 4]
デフォルト値の添え字
以前は、値がnilの場合に備えて、nil合体演算子を使用してデフォルト値を指定するのが一般的でした。
スウィフト3:
let dict = ["a": 1, "b": 5] dict["c"] ?? 0 // 0
Swift 4では、添え字に新しいdefault
値が導入されています(SE-0165の一部)。
let dict = ["a": 1, "b": 5] dict["c", default: 0] // 0, equals to `dict["c"] ?? 0` in Swift 3
デフォルト値で添え字を付けながら辞書を変更することもできます。
let string = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam mattis lorem et leo laoreet fermentum. Mauris pretium enim ac mi tempor viverra et fermentum nisl. Sed diam nibh, posuere non lectus at, ornare bibendum erat. Fusce mattis sem ac feugiat vulputate. Morbi at nunc maximus, vestibulum orci et, dictum neque. Vestibulum vulputate augue ac libero vulputate vestibulum. Nullam blandit et sapien non fermentum. Proin mollis nisl at vulputate euismod. """ var wordsCountByLine = [Int: Int]() let lines = string.split(separator: "\n") for (index, line) in lines.enumerated() { let lineWordsCount = line.split(separator: " ").count wordsCountByLine[index, default: 0] += lineWordsCount } print(wordsCountByLine) // [2: 10, 4: 15, 5: 7, 6: 6, 7: 6, 0: 8, 1: 7, 3: 10]
辞書固有のマップとフィルター
シーケンス要素のグループ化
プロトコルの制約付き関連タイプ
提案SE-0142は、関連する型宣言への条件節の追加を導入しています。
extension Sequence where Element: Numeric { var sum: Element { var result: Element = 0 for element in self { result += element } return result } } [1,2,3,4].sum
アーカイブとシリアル化(エンコード/デコード)
以前は、カスタムタイプをシリアル化するには、古くてよく知られているNSCoding
プロトコルを使用する必要がありました。 問題は、 struct
やenum
などの非クラス型がこのプロトコルに準拠できないことです。そのため、開発者は、 NSCoding
準拠できるネストされたクラスを作成することで、互換性の追加レイヤーを提供するなどのハックを使用する以外に何もしませんNSCoding
。
Swift 4には、SE-0166のおかげでこの問題に対する非常に便利なソリューションがCodable
ますCodable
プロトコルの導入:
struct Employee: Codable { let name: String let age: Int let role: Role enum Role: String, Codable { case manager case developer case admin } } struct Company { let name: String let officeLocation: Location? let employees: [Employee] } struct Location : Codable { let latitude: Double let longitude: Double }
このような単純なケースでは、必要なのはすべてのカスタムタイプにCodable
プロトコル準拠を追加することだけです。コンパイラーがすべての魔法を実行します。 それでおしまい!
Codable
は、 typealias
プロトコルとEncodable
プロトコルをDecodable
たtypealias
ため、たとえば、JSONデータからタイプインスタンスをデコードする場合は、 Decodable
プロトコルへの準拠のみを宣言できます。
エンコーディング
Codable
値をシリアル化または逆シリアル化する場合は、エンコーダーまたはデコーダーオブジェクトを使用する必要があります。 Swift 4には、JSONおよびプロパティリスト用のエンコーダー/デコーダーのセットと、エンコード/デコード中にスローされる可能性のあるさまざまなタイプのエラー用の新しいCocoaError
すでに付属しています。 NSKeyedArchiver
およびNSKeyedUnarchiver
は、 Codable
タイプもサポートしてCodable
ます。
let employee = Employee(name: "Peter", age: 27, role: .manager) let company = Company(name: "Awesome Company", officeLocation: nil, employees: [employee]) let encoder = JSONEncoder() let companyData = try encoder.encode(company) let string = String(data: companyData, encoding: .utf8)! print(string) >>> { "name" : "Awesome Company", "employees" : [ { "name" : "Peter", "age" : 27, "role" : "manager" } ] }
ケーキですね。
デコード
デコーダーは、カスタムCodable
タイプをData
から逆シリアル化するために使用されます。 データ自体からどのタイプをデコードするかがわからないため、デコードするタイプを指定する必要があります(例: Employee
または[Employee]
。
let decoder = JSONDecoder() let jsonData = """ [ { "name" : "Peter", "age" : 27, "role" : "manager" }, { "name" : "Alex", "age" : 26, "role" : "developer" }, { "name" : "Eugene", "age" : 30, "role" : "admin" } ] """.data(using: .utf8)! let employees = try decoder.decode([Employee].self, from: jsonData)
If one of `Codable` type instances fails to decode, then whole collection will fail to decode.
カスタムキー名
ほとんどの場合、カスタムSwiftタイプで使用する名前は、このタイプを表すJSONデータのキーと一致しません。 カスタムタイプのプロパティ名とJSONキーの間のマッピングを作成するには、 CodingKey
プロトコルに準拠する必要があるCodingKeys
という名前のネストされた列挙型を作成できます。
struct Country: Decodable { let id: String let name: String let phoneCode: String private enum CodingKeys: String, CodingKey { case id = "alpha3" case name case phoneCode = "phone_code" } }
カスタムデコード
複雑なケースがある場合は、 Decodable
プロトコルからカスタム初期化子を実装できます。
struct Transaction { let id: Int let action: String let source: String let amount: Int let state: TransactionState let createdAt: Date let authorName: String enum TransactionState: String, Decodable { case done case canceled case processed } } extension Transaction: Decodable { private enum CodingKeys: String, CodingKey { case id case action = "action_name" case source = "source_name" case amount case state case createdAt = "created_at" case author } private enum AuthorKeys: String, CodingKey { case fullName = "full_name" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) actionName = try container.decode(String.self, forKey: .action) sourceName = try container.decode(String.self, forKey: .source) let createdAtValue = try container.decode(Double.self, forKey: .createdAt) createdAt = Date(timeIntervalSince1970: createdAtValue) state = try container.decode(TransactionState.self, forKey: .state) amount = try container.decodeIfPresent(Int.self, forKey: .amount) ?? 0 do { let authorContainer = try container.nestedContainer(keyedBy: AuthorKeys.self, forKey: .author) authorName = try authorContainer.decode(String.self, forKey: .fullName) } catch { authorName = "" } } }
キーバリューコーディング
Swift 4がもたらす便利な機能の1つは、SE-0161で説明されているスマートキーパスです。 強く型付けされておらず、Objective-Cメンバーに対してのみ機能するSwift 3 #keyPath()
とは異なり、Swift 4 KeyPathはジェネリッククラスです。つまり、キーパスが強く型付けされるようになりました。 いくつかの例を見てみましょう。
struct User { var username: String }
\User.username
\<Type>.<path>
の一般的な形式\<Type>.<path>
ここで、 <Type>
はタイプ名であり、 <path>
は1つ以上のプロパティのチェーンです(例: \User.username
。
let user = User(username: "max") let username = user[keyPath: \User.username] // "max"
変更可能な場合は、このキーパスを使用して新しい値を書き込むこともできます。
var user = User(username: "max") user[keyPath: \User.username] = "alex" // "alex"
キーパスは、階層の1つのレベルに限定されません。
struct Comment { let content: String var author: User } let max = User(username: "max") let comment = Comment(content: "Nice post!", author: max) let authorUsername = comment[keyPath: \Comment.author.username] // "max"
キーパスは変数に格納できます。
let authorKeyPath = \Comment.author let usernameKeyPath = authorKeyPath.appending(path: \.username) let authorUsername = comment[keyPath: usernameKeyPath] // "max"
オプションのプロパティと計算されたプロパティにキーパスを使用することもできます。
struct Post { let title: String var comments: [Comment] var topComment: Comment? { return comments.first } } let max = User(username: "max") let alex = User(username: "alex") var post = Post(title: "What's new in Swift 4", comments: []) let topCommentAuthorUsernameKeyPath = \Post.topComment?.author.username post[keyPath: topCommentAuthorUsernameKeyPath] // nil let comment = Comment(content: "", author: alex) let anotherComment = Comment(content: "Nice post!", author: max) post.comments = [comment, anotherComment] post[keyPath: topCommentAuthorUsernameKeyPath] // "alex"
SE-0161は、キーパスのサポート添え字を強調していますが、まだ実装されていません。
post.comments[keyPath: \.[0].content] // error: key path support for subscript components is not implemented let firstCommentAuthorKeyPath = \Post.comments[0].author // error: key path support for subscript components is not implemented
KVO
新しいキーパスに加えて、キー値監視APIもSwift4で更新されました。
New KVO APIs depend on Objective-C runtime and works for `NSObject` subclasses only, so it can't be used for Swift structs and classes which don't inherit `NSObject`. In order to observe property it should be marked as `@objc dynamic var`.
class User: NSObject { @objc dynamic var name: String var username: String init(name: String, username: String) { self.name = name self.userName = userName super.init() } } let user = User(name: "Max", username: "max") let nameObservation = user.observe(\.name, options: [.new, .old]) { user, change in // NSKeyValueObservation if let oldValue = change.oldValue, let newValue = change.newValue { print("fullName has changed from \(oldValue) to \(newValue)") } else { print("fullName is now \(user.name)") } } user.name = "Alex" // name has changed from Max to Alex
観察を停止したい場合はinvalidate()
メソッドを呼び出します
nameObservation.invalidate() user.name = "Elina" // observer isn't get called
また、deinited時に停止するため、保存する場合は、必ずプロパティまたは別の場所に保存してください。
片面レンジ
SE-0172では、既存の範囲演算子のプレフィックス/ポストフィックスバージョンを介して作成された「片側」範囲と、さまざまな種類の範囲を取得するメソッドの作成を簡素化する新しいRangeExpression
プロトコルがRangeExpression
ます。
無限のシーケンス
片側範囲を使用して、無限シーケンスを作成できます。
let letters = ["a", "b", "c", "d"] let numberedLetters = Array(zip(1..., letters)) // [(1, "a"), (2, "b"), (3, "c"), (4, "d")]
let string = "Hello, Mind Studios!" let index = string.index(of: ",")! string[..<index] // "Hello" string[...index] // "Hello,"
パターンマッチングでの片側範囲の使用
let value = 5 switch value { case 1...: print("greater than zero") case 0: print("zero") case ..<0: print("less than zero") default: break }
一般的な添え字
SE-0148添え字に一般的な引数と戻り値を含めることができるようになりました
struct JSON { let data: [String: Any] subscript<T>(key: String) -> T? { return data[key] as? T } } let jsonDictionary: [String: Any] = [ "name": "Ukraine", "flag": "", "population": 42_500_000 ] let json = JSON(data: jsonDictionary) let population: Int? = json["population"] // 42500600
extension Dictionary where Value == String { subscript<T: RawRepresentable>(key: Key) -> T? where T.RawValue == Value { guard let string = self[key] else { return nil } return T(rawValue: string) } } enum Color: String { case red case green case blue } let dictionary = [1: "red"] let color: Color? = dictionary[1] // red
Objective-Cの推論を制限する
Swift 4は、Objective-C(SE-0160)で宣言を使用できるようにする必要がある場合にのみ@objc推論を制限することにより、@ objc推論を最小限に抑えます。
これにより、冗長なObjective-Cコードを使用しない場合はコンパイルしないため、アプリのバイナリサイズが小さくなり、@ objcが推測されるタイミングをより細かく制御できます。 NSObject派生クラスは@objcを推測しなくなりました。
ただし、Swiftコードに暗黙の推論が続く状況がいくつかあります。
@objc属性を持つ宣言
@objcプロトコルの要件を満たす宣言
@ IBAction、@ IBInspectable、@ IBOutlet、@ NSManaged、@ GKInspectable属性を持つ宣言
クラス全体で@objc推論を有効にするには、新しい@objcmembers属性を使用できます。
特定の拡張機能または関数の@objc推論を無効にするには、新しい@nonobjc属性を追加します。
クラスとプロトコルの構成
Swift 4では、他のSwiftタイプと一緒にプロトコルを作成できるようになりました。
User & Codable & CustomStringConvertible typealias MyType = User & Codable & CustomStringConvertible
Swift4の利点。
Swift 4の利点は、Appleが新しい言語バージョンをリリースするときによく発生するため、非常に大きなものです。 言語パフォーマンスの向上とは別に、移行プロセスも大幅に安定しました。 Swift 2.2を3.0に移行するプロセスに思いを馳せて、すべての依存関係を転送するという複雑なプロセスを思い出します。 Swift 4.0の変更により、サードパーティのライブラリを実際に「再配置」せずに残すことができます。Swift自体を更新するだけで済みます。
また、Swift 4.0と3.0の改善に関しては、コンパイルされたバイナリファイルのサイズが変更されたため、アプリのサイズが小さくなりました。 たとえば、モバイルアプリケーションの重量は20 MBでしたが、最新のSwiftバージョンでは約17MBかかります。 そして、Swift4とSwift3の間には基本的な違いがあります。バグ修正が行われ、言語が少し速くなりました。
Swiftが使用されてから何年も経ち、今後のアップデートごとに進化し続けます。 新しい言語が更新されるたびに、これまで知られていなかった新しい開発の視点が生まれ、新しいiOSの地平を探求することを楽しみにしています。
iOS開発用のMVPvs MVC vs MVVM vsVIPERに関する記事をお見逃しなく。
マックスマシュコフとエリナベッサラボワによって書かれました。