Pembungkus properti Swift

Jika Anda menggunakan SwiftUI, Anda mungkin memperhatikan kata kunci seperti @ObservedObject, @EnvironmentObject, @FetchRequest, dan sebagainya. Pembungkus Properti (selanjutnya disebut "pembungkus properti") adalah fitur baru dari Swift 5.1. Artikel ini akan membantu Anda memahami dari mana semua konstruksi dari @ berasal, bagaimana cara menggunakannya di SwiftUI dan dalam proyek Anda.



Diterjemahkan oleh: Evgeny Zavozhansky, pengembang FunCorp.


Catatan: Pada saat terjemahan disiapkan, bagian dari kode sumber artikel asli telah kehilangan relevansinya karena perubahan bahasa, sehingga beberapa contoh kode sengaja diganti.


Pembungkus properti pertama kali diperkenalkan di forum Swift pada Maret 2019, beberapa bulan sebelum pengumuman SwiftUI. Dalam proposal awalnya, Douglas Gregor, anggota tim Swift Core, menggambarkan konstruk ini (kemudian disebut delegasi properti) sebagai "generalisasi yang dapat diakses pengguna dari fungsi yang saat ini disediakan oleh konstruksi bahasa seperti lazy , misalnya."


Jika sebuah properti dideklarasikan dengan kata kunci lazy , ini berarti ia akan diinisialisasi saat pertama kali diakses. Sebagai contoh, inisialisasi properti ditangguhkan dapat diimplementasikan menggunakan properti pribadi, diakses melalui properti yang dihitung. Tetapi menggunakan kata kunci lazy membuat ini lebih mudah.


 struct Structure {    //      lazy    lazy var deferred = โ€ฆ    //            private var _deferred: Type?    var deferred: Type {        get {            if let value = _deferred { return value }            let initialValue = โ€ฆ            _deferred = initialValue            return initialValue        }        set {            _deferred = newValue        }    } } 

SE-0258: Property Wrapper dengan sempurna menjelaskan desain dan implementasi pembungkus properti. Karena itu, alih-alih mencoba meningkatkan deskripsi dalam dokumentasi resmi, pertimbangkan beberapa contoh yang dapat diimplementasikan menggunakan pembungkus properti:


  • pembatasan nilai properti;
  • konversi nilai saat mengubah properti;
  • mengubah semantik persamaan dan membandingkan properti;
  • logging akses properti.

Batasi Nilai Properti


SE-0258: Property Wrapper menyediakan beberapa contoh praktis, termasuk @Clamping , @Copying , @Atomic , @ThreadSpecific , @Box , @UserDefault . Pertimbangkan bungkus @Clamping , yang memungkinkan Anda membatasi nilai maksimum atau minimum suatu properti.


 @propertyWrapper struct Clamping<Value: Comparable> {    var value: Value    let range: ClosedRange<Value>    init(initialValue value: Value, _ range: ClosedRange<Value>) {        precondition(range.contains(value))        self.value = value        self.range = range    }    var wrappedValue: Value {        get { value }        set { value = min(max(range.lowerBound, newValue), range.upperBound) }    } } 

@Clamping dapat digunakan, misalnya, untuk mensimulasikan keasaman suatu larutan, yang nilainya dapat mengambil nilai dari 0 hingga 14.


 struct Solution {    @Clamping(0...14) var pH: Double = 7.0 } let carbonicAcid = Solution(pH: 4.68) 

Mencoba menetapkan nilai pH di luar rentang dari (0...14) akan menyebabkan properti mengambil nilai yang paling dekat dengan interval minimum atau maksimum.


 let superDuperAcid = Solution(pH: -1) superDuperAcid.pH // 0 

Pembungkus properti dapat digunakan untuk mengimplementasikan pembungkus properti lainnya. Misalnya, pembungkus @UnitInterval membatasi nilai properti hingga interval (0...1) menggunakan @Clamping(0...1) :


 @propertyWrapper struct UnitInterval<Value: FloatingPoint> {    @Clamping(0...1)    var wrappedValue: Value = .zero    init(initialValue value: Value) {        self.wrappedValue = value    } } 

Ide serupa


  • @Positive / @NonNegative menunjukkan bahwa nilainya dapat berupa angka positif atau negatif.
  • @NonZero menunjukkan bahwa nilai properti tidak boleh 0.
  • @Validated atau @Whitelisted / @Blacklisted membatasi nilai properti ke nilai tertentu.

Mengkonversi Nilai Saat Mengubah Properti


Memvalidasi nilai-nilai bidang teks adalah sakit kepala konstan untuk pengembang aplikasi. Ada begitu banyak hal yang harus dilacak: mulai dari kata-kata hampa seperti pengodean hingga upaya jahat untuk memasukkan kode melalui bidang teks. Pertimbangkan untuk menggunakan pembungkus properti untuk menghapus spasi yang telah dimasukkan pengguna di awal dan di akhir baris.


 import Foundation let url = URL(string: " https://habrahabr.ru") // nil let date = ISO8601DateFormatter().date(from: " 2019-06-24") // nil let words = " Hello, world!".components(separatedBy: .whitespaces) words.count // 3 

Foundation menawarkan metode trimmingCharacters(in:) , yang dengannya Anda dapat menghapus spasi di awal dan akhir baris. Anda dapat memanggil metode ini kapan pun Anda perlu menjamin kebenaran input, tetapi itu tidak terlalu nyaman. Anda dapat menggunakan pembungkus properti untuk ini.


 import Foundation @propertyWrapper struct Trimmed {    private(set) var value: String = ""    var wrappedValue: String {        get { return value }        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } 

 struct Post {    @Trimmed var title: String    @Trimmed var body: String } let quine = Post(title: " Swift Property Wrappers ", body: "โ€ฆ") quine.title // "Swift Property Wrappers" โ€”        quine.title = "   @propertyWrapper " // "@propertyWrapper" 

Ide serupa


  • @Transformed menerapkan konversi ICU ke string @Transformed .
  • @Rounded / @Truncated rounds atau memotong nilai string.

Ubah semantik kesetaraan dan perbandingan properti


Dalam Swift, dua string sama jika mereka setara secara kanonik , yaitu mengandung karakter yang sama. Tapi misalkan kita ingin properti string sama, tidak peka huruf besar kecil, yang dikandungnya.


@CaseInsensitive mengimplementasikan pembungkus untuk properti tipe String atau SubString .


 import Foundation @propertyWrapper struct CaseInsensitive<Value: StringProtocol> {    var wrappedValue: Value } extension CaseInsensitive: Comparable {    private func compare(_ other: CaseInsensitive) -> ComparisonResult {        wrappedValue.caseInsensitiveCompare(other.wrappedValue)    }    static func == (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedSame    }    static func < (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedAscending    }    static func > (lhs: CaseInsensitive, rhs: CaseInsensitive) -> Bool {        lhs.compare(rhs) == .orderedDescending    } } 

 let hello: String = "hello" let HELLO: String = "HELLO" hello == HELLO // false CaseInsensitive(wrappedValue: hello) == CaseInsensitive(wrappedValue: HELLO) // true 

Ide serupa


  • @Approximate untuk perbandingan kasar dari properti tipe Double atau Float.
  • @Ranked untuk properti yang nilainya berurutan (misalnya, pangkat kartu remi).

Pencatatan Akses Properti


@Versioned akan memungkinkan Anda untuk mencegat nilai yang ditugaskan dan mengingat kapan mereka ditetapkan.


 import Foundation @propertyWrapper struct Versioned<Value> {    private var value: Value    private(set) var timestampedValues: [(Date, Value)] = []    var wrappedValue: Value {        get { value }        set {            defer { timestampedValues.append((Date(), value)) }            value = newValue        }    }    init(initialValue value: Value) {        self.wrappedValue = value    } } 

Kelas ExpenseReport memungkinkan ExpenseReport menyimpan stempel waktu status pemrosesan dari laporan pengeluaran.


 class ExpenseReport {    enum State { case submitted, received, approved, denied }    @Versioned var state: State = .submitted } 

Tetapi contoh di atas menunjukkan batasan serius dalam implementasi pembungkus properti saat ini, yang mengikuti dari pembatasan Swift: properti tidak dapat membuang pengecualian. Jika kami ingin menambahkan batasan ke @Versioned untuk mencegah nilai berubah menjadi .approved setelah mengambil nilai .denied , maka opsi terbaik adalah fatalError() , yang tidak cocok untuk aplikasi nyata.


 class ExpenseReport {    @Versioned var state: State = .submitted {        willSet {            if newValue == .approved,                $state.timestampedValues.map { $0.1 }.contains(.denied)            {                fatalError("")            }        }    } } var tripExpenses = ExpenseReport() tripExpenses.state = .denied tripExpenses.state = .approved // Fatal error: ยซยป   . 

Ide serupa


  • @Audited untuk @Audited akses properti.
  • @UserDefault untuk merangkum mekanisme untuk membaca dan menyimpan data di UserDefaults .

Keterbatasan


Properti tidak dapat membuang pengecualian


Seperti yang telah disebutkan, pembungkus properti hanya dapat menggunakan beberapa metode untuk memproses nilai yang tidak valid:


  • abaikan mereka;
  • menghentikan aplikasi menggunakan fatalError ().

Properti yang dibungkus tidak dapat ditandai dengan atribut `typealias`


Contoh @UnitInterval atas, yang propertinya dibatasi oleh interval (0...1) , tidak dapat dinyatakan sebagai


 typealias UnitInterval = Clamping(0...1) 

Pembatasan penggunaan komposisi beberapa pembungkus properti


Menulis pembungkus properti bukanlah operasi komutatif: urutan deklarasi akan memengaruhi perilaku. Pertimbangkan contoh di mana properti siput, yang merupakan url posting blog, dinormalisasi. Dalam hal ini, hasil normalisasi akan bervariasi tergantung pada kapan spasi diganti dengan tanda hubung, sebelum atau setelah penghapusan spasi. Karenanya, saat ini, komposisi beberapa pembungkus properti tidak didukung.


 @propertyWrapper struct Dasherized {    private(set) var value: String = ""    var wrappedValue: String {        get { value }        set { value = newValue.replacingOccurrences(of: " ", with: "-") }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } struct Post {    โ€ฆ    @Dasherized @Trimmed var slug: String // error: multiple property wrappers are not supported } 

Namun, batasan ini dapat dielakkan dengan menggunakan pembungkus properti bersarang.


 @propertyWrapper struct TrimmedAndDasherized {    @Dasherized    private(set) var value: String = ""    var wrappedValue: String {        get { value }        set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }    }    init(initialValue: String) {        self.wrappedValue = initialValue    } } struct Post {    โ€ฆ    @TrimmedAndDasherized var slug: String } 

Pembatasan pembungkus properti lainnya


  • Tidak dapat digunakan di dalam protokol.
  • Contoh properti wrapper tidak dapat dideklarasikan dalam enum .
  • Properti terbungkus yang dideklarasikan di dalam kelas tidak dapat diganti oleh properti lain.
  • Properti terbungkus tidak boleh lazy , @NSCopying , @NSManaged , weak atau unowned .
  • Properti terbungkus harus menjadi satu-satunya dalam definisi (mis., @Lazy var (x, y) = /* ... */ ).
  • Properti terbungkus tidak dapat memiliki getter dan setter ditentukan.
  • Jenis properti wrappedValue dan variabel wrappedValue di init(wrappedValue:) harus memiliki tingkat akses yang sama dengan tipe wrapper properti.
  • Tipe properti dari projectedValue harus memiliki tingkat akses yang sama dengan tipe pembungkus dari properti.
  • init() harus memiliki tingkat akses yang sama dengan tipe pembungkus properti.

Mari kita simpulkan. Pembungkus properti di Swift memberi penulis perpustakaan akses ke perilaku tingkat tinggi yang sebelumnya disediakan untuk fungsi bahasa. Potensi mereka untuk meningkatkan keterbacaan dan mengurangi kompleksitas kode sangat besar, dan kami hanya memeriksa kemampuan alat ini secara dangkal.


Apakah Anda menggunakan pembungkus properti di proyek Anda? Tulis di komentar!

Source: https://habr.com/ru/post/id485008/


All Articles