آموزش سوئیفت (Swift): کاربرد Enum با ژنریک و بستار – بخش پانزدهم
در این بخش از سری مقالات آموزش برنامهنویسی سوئیفت قصد داریم به صورت فشرده برخی از مفاهیم مهم این زبان برنامهنویسی شامل استفاده از Enum به همراه ژنریک و بستارها را با هم ترکیب کنیم و با روش عملی استفاده از آنها در کدنویسی آشنا شویم.
در بخش قبلی با مبانی روشهای ایجاد خطاهای سفارشی و استفاده از آنها در کد برای جلوگیری از کرش کردن برنامه آشنا شدیم. برای مطالعه بخش قبلی میتوانید به لینک زیر مراجعه کنید:
Enum به همراه ژنریک و بستار
در بخشهای قبلی این سری آموزشی در مورد Enum-ها صحبت کردیم و گفتم که Enum گزینههای مختلفی در اختیار شما قرار میدهد که میتوانید از میان آنها انتخاب کنید و به نوعی حالتهای مختلف را منحصر به آن گزینهها بکنید.
ما میتوانیم از مقادیر متناظر با حالتهای Enum برای تعریف کردن نوعی که در زمان استفاده از Enum وهلهسازی خواهد شد استفاده کنیم.
1enum Location {
2 case address(String)
3 case coordinate(Double, Double)
4}
5
6let freddysLocation = Location.address("123 Elm Street")
7let jasonsLocation = Location.coordinate(42.235140, -88.355958)
چنان که میبینید امکان بسیار جالبی است. اگر ندانیم چه نوعی وارد خواهد شد، میتوانیم از ژنریک استفاده کنیم.
1enum Location<T> {
2 case address(T)
3 case coordinate(T)
4}
5
6let freddysLocation = Location.address("123 Elm Street")
7let jasonsLocation = Location.coordinate(42.235140, -88.355958)
8
9// or we could use CLLocationCoordinate2d from CoreLocation
10let jasonsCLLocation = Location.coordinate(
11 CLLocationCoordinate2d(latitude: 42.235140,
12 longitude: -88.355958))
بدین ترتیب میتوانیم در زمان ایجاد یک address یا coordinate از هر نوع که میخواهیم، استفاده کنیم.
اکنون به بررسی روش استفاده ترکیبی از Enum به همراه ژنریک و بستار میپردازیم. این ترفند جالبی است که برنامهنویسان حرفهای از آن در کدهای خود استفاده میکنند.
1struct Download {
2 enum Result<T> {
3 case success(T)
4 case failure(error)
5 }
6
7 func start(_ completion: @escaping (Result<T>) -> Void) {
8 let session = URLSession(configuration: .ephemeral)
9 let url = URL(string: "http://www.example.com")!
10
11 let task = session.dataTask(with: url) {
12 data, response, error in
13 guard error == nil else {
14 DispatchQueue.main.async {
15 defer { session.invalidateAndCancel() }
16 completion(.failure(error!))
17 }
18 return
19 }
20
21 if let httpResponse = response as? HTTPURLResponse {
22 if httpResponse.statusCode == 200 {
23 DispatchQueue.main.async {
24 completion(.success(data!))
25 }
26 }
27 }
28 }
29
30 task.resume()
31 }
32}
البته اگر بگوییم درک طرز کار این روش آسان است، دروغ گفتهایم. حتی با وجود تورفتگیها، میبینیم که درک کد فوق دشوار است. اما جای نگرانی نیست، زیرا در ادامه، همه این موارد را جزء به جزء توضیح میدهیم.
Struct Download
این Struct منطق ما را نگهداری میکند. این Struct میتواند یک کلاس باشد، زیرا وظیفه اجرای فراخوانیهای شبکه را بر عهده دارد.
1enum Result<T> {
2 case success(T)
3 case failure(Error)
4}
بدین ترتیب دو گزینه در اختیار ما قرار میگیرد که یکی (success(anything. و دیگری (failure(someError. است.
1func start(_ completion: @escaping (Result<T>) -> Void) { }
این متدی است که یک تابع میگیرد. آن تابع یک حالت را از Enum به نام Result میگیرد و چیزی هم بازگشت نمیدهد.
let session
این دستور یک «نشست» (Session) از URLSession با یک پیکربندی ephemeral میسازد. منظور از ephemeral این است که تنها در حافظه وجود دارد و به عبارتی معادل مرور خصوصی وب است.
let url
این دستور یک URL از رشتهای که به آن ارسال کردهایم، میسازد. این رشته میتواند هر صفحه وبی که میخواهید از آن دانلود کنید باشد.
1let task = session.dataTask(with:URL) { data، response، error in ... }
کد فوق وظیفهای در اختیار ما قرار میدهد که با آن میتوانیم دادههای مورد نظر خود را دانلود کنیم. آن را میتوان مانند اسبی تصور کرد که میتوانیم آن را به هر کجا که میخواهیم برانیم. Data شامل دادههای باینری (0 و 1) است که دریافت میکنیم. response هدرهای پاسخی است که دریافت میشود و در ادامه در مورد آن بیشتر توضیح میدهیم.
error در صورت ناموفق بودن درخواست بازگشت مییابد و درک این نکته مهم است. چون در صورت دریافت یک خطای 404 (صفحه یافت نشد) در فراخوانی، میتوانید اطلاعات مربوطه را از response دریافت کنید. حتی اگر خطای 500 دریافت شود که به معنی ناموفق بودن چیزی در سرور است همچنان میتوان آن را در response مشاهده کرد. error برای ما به این معنی است که نتوانستهایم آن کاری را که میخواستیم اجرا کنیم و حتی درخواست را مقداردهی کنیم. بنابراین error به معنی خطای ما و نه خطای دیگران است.
1guard error == nil else { ... }
این یک بررسی قبل از اجرا محسوب میشود. اگر خطایی دریافت شود، دیگر نیازی به اجرا نخواهد بود و کافی است با بازگشت خطا از همینجا خارج شویم.
ما قبلاً در مورد DispatchQueue.main.async صحبت کردهایم، بنابراین در اینجا میخواهیم فقط کد زیر را توضیح دهیم:
1defer { session.invalidateAndCancel() }
اعتبارزدایی
این کد اقدام به اعتبارزدایی و لغو میکند، یعنی هر کاری که انجام میدادید را متوقف کرده و session را پاک میکند، چون دیگر نیازی به آن نداریم. اما اگر بخواهیم defer را توضیح دهیم، باید بگوییم که defer برای اجرا کردن یک قطعه کد استفاده میشود و مهم نیست که چه کاری انجام مییابد، صرفاً باید قبل از اجرا شدن، تا زمانی که متد پایان مییابد صبر کند. در واقع شبیه به یک deinit برای متدها، تابعها و بستارها است.
سپس از (!completion(.failure(error استفاده میکنیم. completion از نام پارامتر در start میآید. failure. حالتی از Enum با نام Result و !error خطای به اجبار باز شده است که از بستار دریافت شده است. در این موقعیت این بهکارگیری اجبار، کار درستی محسوب میشود، چون قبلاً تهی نبودن آن را بررسی کردهایم و از آنجا که این کد اجرا میشود، به این معنی است که تهی نبوده است. در ادامه بررسیهای دیگری را نیز اجرا میکنیم.
1if let httpResponse = response as? HTTPURLResponse
دسترسی مستقیم به حالت
متأسفانه سوئیفت دسترسی مستقیم به «حالت» (State) کد ایجاد نمیکند؛ اما HTTPURLResponse چنین امکانی در اختیار ما قرار میدهد و میتوانیم نوع پاسخ را به یک HTTPURLResponse تغییر دهیم و باید موفق باشد. در این حالت بیدرنگ بررسی میکنیم که آیا پاسخ موفقی به صورت زیر داریم یا نه:
1if httpResponse.statusCode == 200
اگر هر دوی آنها درست باشند، در این صورت میتوانیم دادهها را به صورت امنی باز پس بفرستیم تا تجزیه شوند و یا هر کار دیگری که قصد انجام آن وجود دارد اجرا شود. ابتدا با استفاده از DispatchQueue.main.async مطمئن میشویم که این کار را روی صف اصلی انجام میدهیم و سپس از دستگیره completion استفاده میکنیم تا این کار را با ((!completion(.success(data به صورت باز کردن اجباری دادهها اجرا کنیم، چون هر سه پارامتر بستار، مقادیر غیر optimal هستند.
در انتهای تابع Start اقدام به فراخوانی ()task.resume میکنیم که وظیفه داده را اجرا میکند. زمانی که این فراخوانی پایان یافت، همه آن کد را که قبلاً بررسی کردیم اجرا میکنیم.
برای این که متوجه شوید همه این موارد در سمت دیگر که Start را فراخوانی میکنیم، چه طور به نظر میرسند، میتوانید کد زیر را ملاحظه کنید:
1start() { result in
2 switch result
3 case .success(let data):
4 parse(data)
5 case .failure(let error):
6 print(error.localizedDescription)
7 }
8}
سخن پایانی
بدین ترتیب در این مقاله با ارائه یک مثال با روش اجرای یک فراخوانی شبکه آشنا شدیم. روش استفاده از قدرت Enum-ها به همراه تابعها و ژنریک ها برای کمک به بازگشت بستار نمایش یافت. همچنین نگاهی به escaping@ داشتیم و با طرز استفاده از آن بیشتر آشنا شدیم.
این راهحل شبکه یک راهحل بهینه نیست و صرفاً یکی از راهحلهای ممکن محسوب میشود. روشهای مختلفی برای اجرای این کار وجود دارد و بسته به شیوه استفاده از بستارها در Enum-ها ممکن است مسیرهای متفاوتی ایجاد شود.
با این که ممکن است در نگاه نخست کمی دشوار به نظر برسد، اما اگر چند بار آن را تمرین کنید با طرز کار آن آشنا میشوید. البته امکان تکمیل خودکار کد نیز در صورتی که به آن توجه داشته باشید کمک زیادی به این فرایند یادگیری میکند.