Các Câu Hỏi Và Trả Lời Cho Buổi Phỏng Vấn Swift

Đình Văn
16 min readMay 18, 2019

Ngôn ngữ Swift chỉ vừa mới bốn năm tuổi nhưng nó đã trở thành ngôn ngữ mặc định cho lập trình iOS. Vì Swift đã phát triển đến phiên bản 5.0, nó trở thành một ngôn ngữ phức tạp và mạnh mẽ bao hàm các mô hình lập trình hướng đối tượng và hướng chức năng. Mỗi phiên bản mới lại mang đến nhiều sự hoàn thiện và cải tiến tốt hơn.

Nhưng bạn thực sự hiểu biết về Swift đến nhường nào? Trong bài viết lần này, bạn sẽ tìm thấy một vài mẫu câu hỏi phỏng vấn Swift.

Bạn có thể sử dụng những câu hỏi này để phỏng vấn các ứng cử viên để kiểm tra kiến thức Swift của họ. Hoặc bạn có thể kiểm tra chính bạn! Nếu bạn không biết câu trả lời, đừng lo lắng: Mỗi câu hỏi đều có một giải pháp bên dưới mà bạn có thể học hỏi thêm.

Bạn sẽ tìm thấy các câu hỏi được chia thành ba cấp độ khác nhau:

  • Mới bắt đầu: Phù hợp cho những người mới bắt đầu học Swift. Bạn đã đọc một hoặc hai quyển sách nói về chủ đề này hoặc đã làm việc với Swift trên những ứng dụng của chính bạn.
  • Trung cấp: Phù hợp cho những người có niềm yêu thích lớn với ngôn ngữ này. Bạn đã và đang đọc rất nhiều sách về Swift và đã có kinh nghiệm về nó nhiều hơn.
  • Nâng cao: Phù hợp cho các lập trình viên dày dặn kinh nghiệm — những người thích khám phá kỹ lưỡng ngôn ngữ và sử dụng các kỹ thuật tiên tiến.

Tại mỗi cấp độ, bạn sẽ tìm thấy hai dạng câu hỏi:

  • Câu hỏi viết: Tốt cho các bài kiểm tra lập trình qua email, vì chúng có thể liên quan đến việc viết code
  • Câu hỏi nói: Tốt cho việc hỏi qua điện thoại hoặc phỏng vấn trực tiếp, bởi vì ứng cử viên của bạn có thể trả lời chúng bằng lời nói.

Trong khi bạn làm việc với các câu hỏi và câu trả lời, hãy giữ cho Playground luôn mở do đó bạn có thể chạy trực tiếp các đoạn code được đính kèm với câu trả lời trước khi bạn trả lời. Chúng tôi đã kiểm tra tất cả các đáp lại với Xcode 10.2 và Swift 5.

Các Câu Hỏi Viết Cho Người Mới Bắt Đầu

Câu hỏi #1

Xem xét đoạn code sau:

Các giá trị của tutorial1.difficulty tutorial2.difficulty là gì? Sẽ có gì khác nếu Tutorial là một class? Tại sao hoặc tại sao không?

tutorial1.difficulty bằng 1, trong khi tutorial2.difficulty bằng 2

Structures trong Swift là kiểu giá trị (value type). Bạn sao chép kiểu giá trị bằng giá trị thay vì tham chiếu. Đoạn code bên dưới tạo một bảo sao chép của tutorial1 và gán nó cho tutorial2:

Sử thay đổi với tutorial2 không được tham chiếu đến tutorial1.

Nếu Tutorial là một class, cả tutorial1.difficultytutorial2.difficulty sẽ bằng 2. Class trong Swift là kiểu tham chiếu (reference type). Khi bạn thay đổi thuộc tính của tutorial1, bạn sẽ thấy nó tham chiếu đến tutorial2 và ngược lại.

Câu hỏi #2

Bạn khởi tạo biến view1 với var và bạn khởi tạo biến view2 với let. Sự khác nhau là gì? Liệu dòng cuối cùng sẽ được biên dịch?

Đúng, dòng cuối cùng sẽ được biên dịch. view1 là một biến và bạn có thể gán cho nó một instance mới của UIView. Với let, bạn có thể khởi tạo giá trị một lần duy nhất, do đó đoạn code bên dưới sẽ không được biên dịch:

Tuy nhiên, UIView là một class với kiểu tham chiếu (reference semantics), do đó bạn có thể thay đổi các thuộc tính của view2 — điều đó có nghĩa là dòng cuối cùng sẽ được biên dịch:

Câu hỏi #3

Đoạn code phức tạp này sắp xếp một mảng các tên gọi theo thứ tự alphabet. Đơn giản hóa biểu thức closure nhất mà bạn có thể.

Hệ thống tự suy kiểu tính toán một cách tự động cả kiểu của các tham số đầu vào trong biểu thức closure lẩn kiểu trả về, do đó bạn có thể bỏ qua chúng:

Bạn có thể thay thế kí hiệu $i cho các tên tham số đầu vào:

Trong các biểu thức closure đơn, bạn có thể lược bỏ từ khóa return. Giá trị của biểu thức cuối cùng trở thành giá trị trả về của closure đó:

Cuối cùng, bởi vì Swift biết rằng các element của mảng conform với Equatable, bạn đơn giản chỉ cần viết:

Câu hỏi #4

Đoạn code này tạo ra hai classs: AddressPerson. Nó sau đó tạo ra hai instance kiểu Person để đại diện cho Ray và Brian.

Giả định Brian di chuyển đến một toàn nhà mới bên kia con đường, bạn sẽ muốn cập nhật hồ sơ của anh ấy như vậy:

Nó biên dịch và chạy không lỗi. Nếu bạn kiểm tra địa chỉ của Ray ngay bây giờ, cậu ấy cũng di chuyển đến tòa nhà mới.

Chuyện gì đã xảy ra ở đây? Làm sao để bạn có thể fix nó?

Address là một class và có kiểu tham chiếu (reference semantics) do đó headquarters là cùng một instance, bất kể bạn truy xuất nó thông qua ray hay brian. Việc thay đổi địa chỉ của headquarters sẽ thay đó nó ở cả hai. Bạn có thể tưởng tượng điều gì sẽ xảy ra nếu Brian nhận được thư gửi cho Ray hoặc ngược lại? :)

Giải pháp là tạo một Address mới và gán nó cho Brian, hoặc định nghĩa lại Address là một struct thay vì class.

Tốt lắm, nhưng bạn chưa thể trở thành Jadi được đâu. Bạn sẽ làm như thế nào với nhiều câu hỏi đóng mở trên lý thuyết và thực hành?

Để trả lời những loại câu hỏi này, bạn phải cần thực hành các đoạn code trên playground.

Các Câu Hỏi Nói Cho Người Mới Bắt Đầu

Câu hỏi #1

Optional là gì và đâu là vấn đề mà optional giải quyết?

Một optional cho phép một biến của bất kì kiểu dữ liệu nào có thể đại diện cho việc không có giá trị nào. Trên Objective-C, sự vắng mặt của giá trị là chỉ có sẵn trên các kiểu tham chiếu sử dụng giá trị đặc biệt nil. Kiểu giá trị, ví dụ như int hay float, không có khả năng đó.

Swift mở rộng khái niệm không có giá trị trên cả kiểu tham chiếu và kiểu giá trị với Optional. Một biến optional có thể giữ cả giá trị hoặc nil, nghĩa là không có giá trị.

Câu hỏi #2

Tổng quan sự khác nhau giữa một structure và một class.

Bạn có thể tổng quan sự khác nhau như sau:

  • Class hỗ trợ kế thừa, còn structure thì không.
  • Class là kiểu tham chiếu, còn structure là kiểu giá trị.

Câu hỏi #3

Generic là gì và đâu là vấn đề mà nó giải quyết?

Trong Swift, bạn có thể sử dụng generic trong cả các hàm và kiểu dữ liệu, ví dụ trong các class, structure hay enum.

Generic giải quyết vấn đề code bị trùng lặp. Khi bạn có một phương thức mà truyền vào một kiểu tham số, sau đó bạn tạo lại cùng phương thức đó mà truyền vào một kiểu tham số khác.

Ví dụ, trong đoạn code dưới đây, hàm thứ hai clone lại hàm đầu tiên, ngoại trừ việc nó chấp nhận đầu vào là các string thay vì các integer.

Bằng việc triển khai generic, bạn có thể gộp hai hàm thành một mà vẫn giữ được type safe cùng lúc. Đây là sự triển khai cho generic:

Bởi vì trong trường hợp này bạn đang kiểm tra sự bằng nhau, bạn bị hạn chế các tham số truyền vào là bất kì kiểu nào mà phải có triển khai phương thức Equatable. Đoạn code này đạt được kết quả như mong muốn và tránh việc truyền các tham số ở một kiểu khác.

Câu hỏi #4

Trong một vài trường hợp, bạn không thể tránh việc sử dụng các optional unwrap ngầm định. Khi nào? Tại sao?

Các lý do phổ biến nhất khi sử dụng các optional unwrap ngầm định là:

  1. Khi bạn không thể khởi tạo một thuộc tính mà bình thường nó không nil lúc khởi tạo. Một ví dụ điển hình là một Interface Builder outlet, nó luôn khởi tạo sau owner của nó. Trong trường hợp này — giả định rằng nó được thiết lập đúng cách trong Interface Builder — bạn đã đảm bảo rằng outlet sẽ là không nil trước khi bạn sử dụng nó.
  2. Để giải quyết vấn đề vòng tham chiếu mạnh (strong reference cycle problem), nó là khi hai instance tham chiếu lẫn nhau và yêu cầu một tham chiếu không-nil đến instance khác. Trong trường hợp này, bạn đánh dấu một bên tham tham chiếu bằng unowned, trong khi bên kia bạn sử dụng option unwrap ngầm định.

Câu hỏi #5

Nếu những cách để unwrap một optional? Chúng được đánh giá về mặt an toàn như thế này?

Gợi ý: Có 7 cách.

Forced unwrapping — không an toàn

Định nghĩa biến unwrapped ngầm định (Implicitly unwrapped) — không an toàn trong nhiều trường hợp.

Optional binding — an toàn

Optional chaining — an toàn

Toán tử kết hợp nil (Nil coalescing) — an toàn

Biểu thức Guard — an toàn

Optional pattern — an toàn

Các Câu Hỏi Viết Trung Cấp

Bây giờ, hãy chuyển đến các câu hỏi khó hơn một chút. Bạn đã sẵn sàng chưa?

Câu hỏi #1

Sự khác nhau giữa nil.none là gì?

Không có sự khác nhau. Bởi vì Optional.none (viết ngắn gọn .none) và nil là một.

Sự thật, biểu thức so sánh dưới đây sẽ ra kết quả là true:

Việc sử dụng nil phổ biến hơn và nó là cấu trúc convention được khuyến khích dùng.

Câu hỏi #2

Đây là một model của một nhiệt kế theo kiểu class và kiểu struct. Trình biên dịch sẽ cảnh báo lỗi về dòng cuối cùng. Tại sao nó lại lỗi khi biên dịch?

Chú ý: Đọc code một cách cẩn thận và nghĩ về nó trước khi test nó trên một playground.

ThermometerStruct đã được định nghĩa chính xác với một hàm mutating để thay đổi giá trị biến temperature bên trong nó. Trình biên dịch phát ra lỗi bởi vì bạn đã thực hiện lời gọi registerTemperature trong một instance được tạo thông qua let, nó là bất biến, không thể thay đổi (immutable). Đổi let thành var để làm cho ví dụ trên được biên dịch.

Với cấu trúc structure, bạn phải đánh dấu các phương thức làm thay đổi trạng thái bên trong bằng mutating, nhưng bạn không thể gọi chúng từ các biến bất biến (immutable variables).

Câu hỏi #3

Đoạn code bên dưới in ra cái gì và vì sao?

Nó sẽ in ra: I love cars. Capture list tạo một bản sao chép của thing khi bạn định nghĩa closure. Điều này có nghĩa là giá trị được chụp lại không thay đổi kể cả nếu bạn gán giá trị mới cho thing.

Nếu bạn bỏ qua capture list trong closure, sau đó trình biên dịch sử dụng một tham chiếu thay vì một bản copy. Do đó, khi bạn thực hiện việc gọi closure, nó sẽ chiếu bất kì thay đổi nào đến biến. Bạn có thể nhìn thấy điều đó trong đoạn code bên dưới:

Câu hỏi #4

Đây là một global function đếm số lượng các giá trị duy nhất có trong một mảng:

Mình sử dụng sorted, bởi vậy cần hạn chế T về chỉ những kiểu dữ liệu mà conform với Comparable.

Bạn gọi nó như vậy:

Viết lại hàm trên như một phương thức extension trong Array mà sau đó bạn có thể viết lại lời gọi hàm như thế này:

Bạn có thể viết lại tổng thể countUniques(_:) như một Array extension:

Lưu ý rằng phương thức mới chỉ có sẵn khi kiểu generic Element conform với Comparable.

Câu hỏi #5

Đây là một hàm để chia hai optional kiểu double. Có ba điều kiện trước để xác nhận trước khi thực hiện phép chia thực sự:

  • Số bị chia phải chứ giá trị không nil.
  • Số chia phải chứa giá trị không nil.
  • Số chia không phải là 0.

Cải tiến hàm trên bằng cách sử dụng biểu thức guard không sử dụng forced unwrapping.

Biểu thức guard được giới thiệu trong phiên bản Swift 2.0 cung cấp một lối đi kết thúc khi điều kiện không được thõa mãn. Nó rất hữu ích khi kiểm tra điều kiện trước bởi vì nó cho phép bạn trình bày nó một cách rõ ràng — trách được sự rắc tối của các biểu thức if lồng nhau. Đây là một ví dụ:

Bạn cũng có thể sử dụng biểu thức guard cho optional binding, nó làm cho biến unwrap và có thể truy cập sau biểu thức guard:

Do đó bạn có thể viết lại hàm divide như sau:

Để ý sự có mặt của các toán tử unwrap ngầm định trong dòng cuối cùng bởi vì bạn đã unwrap cả dividenddivisor and lưu chúng trong các biến non-optional không thể thay đổi.

Lưu ý rằng các kết quả của các optional được unwrap trong một biểu thức guard là là có sẵn cho phần còn lại của block code mà biểu thức này xuất hiện.

Bạn có thể đơn giản hóa hơn nữa bằng cách nhóm các biểu thức guard lại với nhau:

Câu hỏi #6

Viết lại phương thức từ câu hỏi số 5 bằng cách sử dụng biểu thức if let.

Biểu thức if let cho phép bạn unwrap optional và sử dụng giá trị của nó bên trong block code của nó. Lưu ý rằng bạn không thể truy xuất optional được unwrap ở bên ngoài block đó. Bạn có thể viết hàm đó sử dụng biểu thức if let như sau:

Các Câu Hỏi Nói Trung Cấp

Câu hỏi #1

Trong Objective-C, bạn định nghĩa một hằng số như sau:

Còn đây là trong Swift:

Đâu là điểm khác nhau giữa chúng?

Một const là một biến được khởi tạo tại thời điểm biên dịch với một giá trị hoặc một biểu thức mà nó phải được thực thi tại thời điểm biên dịch.

Một hằng số được tạo với let là một hằng số được xác định tại thời điểm thực thi. Bạn có thể khởi tạo nó với một biểu thức static hoặc dynamic. Nó cho phép một định nghĩa như bên dưới:

Lưu ý rằng bạn chỉ có thể gán giá trị cho nó một lần duy nhất.

Câu hỏi #2

Để định nghĩa một thuộc tính hay một hàm static, bạn sử dụng modifier static trên các kiểu giá trị. Đây là ví dụ cho một structure:

Cho các class, bạn có thể sử dụng modifier static hoặc class. Chúng đạt được cùng một mục đích, nhưng theo các cách khác nhau. Bạn có thể giải thích sự khác nhau giữa chúng?

static làm cho thuộc tính hoặc hàm trở nên tĩnhkhông thể ghi đè. Việc sử dụng class giúp cho bạn có thể ghi đè thuộc tính hoặc hàm.

Khi sử dụng vào class, static trở thành một ánh xạ của class final.

Ví dụ, trong đoạn code bên dưới, trình biên dịch sẽ báo lỗi khi bạn cố gắng ghi đè illuminate():

Câu hỏi #3

Bạn có thể thêm một stored property vào một kiểu dữ liệu bằng việc sử dụng một extension không? Làm cách nào hoặc tại sao không?

Không, điều đó là không thể. Bạn có thể sử dụng một extension để thêm một hành vi mới cho một kiểu đang tồn tại, nhưng không thể làm thay đổi chính kiểu dữ liệu hay interface của nó. Nếu bạn thêm một stored property, bạn sẽ cần bộ nhớ để lưu trữ giá trị mới. Một extension không thể quản lý tác vụ như vậy.

Câu hỏi #4

Protocol trong Swift là gì?

Một protocol là một kiểu mà nó định nghĩa một bản thiết kế của các phương thức, thuộc tính hoặc các yêu cầu khác. Một class, structure hay enum sau đó có thể thừa kế nó để triển khai các yêu cầu của protocol đó.

Một kiểu đữ liệu triển khai các chi tiết trong một protocol thì được gọi là conform protocol đó. Protocol không tự triển khai bất kì chức năng nào, mà nó chỉ xác định các chức năng. Bạn có thể mở rộng một protocol để cung cấp các triển khai mặc định của một vài trong số các yêu cầu hoặc chức năng bổ sung mà các kiểu conform nó có thể tận dụng được sau đó.

Các Câu Hỏi Viết Nâng Cao

Câu hỏi #1

Xem xét đoạn structure dưới đây mô tả model của một nhiệt kế:

Để tạo một instance, bạn có thể sử dụng đoạn code sau:

Nhưng sẽ tốt hơn nếu khởi tạo nó bằng cách này:

Có thể không? Làm như thế nào?

Swift định nghĩa các protocol mà cho phép bạn có thể khởi tạo một kiểu dữ liệu với các literal values bằng việc sử dụng toán tử gán. Áp dụng các giao thức tương ứng và cung cấp một hàm khởi tạo public cho phép khởi tạo giá trị (literal initialization) của bất kì kiểu dữ liệu nào. Trong trường hợp của Thermometer, bạn triển khai ExpressibleByFloatLiteral như bên dưới:

Bây giờ bạn có thể tạo một instance bằng việc sử dụng một số thập phân float.

Câu hỏi #2

Swift có một tập các toán tử định nghĩa trước để thực hiện các phép tính logic và toán học. Nó còn cho phép tạo ra các toán tử custom, unary hay binary.

Định nghĩa và triển khai một toán tử custom mũ ^^ với các thông số chi tiết sau:

  • Lấy hai tham số đầu vào kiểu Int
  • Trả về tham số thứ nhất mũ lần tham số thứ hai.
  • Đánh giá chính xác biểu thức bằng cách sử dụng thứ tự đại số chuẩn của các phép toán.
  • Bỏ qua khả năng xảy ra các lỗi tràn.

Bạn tạo một toán tử custom mới trong hai bước: Định nghĩa và triển khai.

Bước định nghĩa sử dụng từ khóa operator để xác định kiểu (unary hay binary), chuỗi các kí tự sản sinh ra toán tử đó, tính kết hợp và ưu tiên của nó. Swift 3.0 thay đổi việc triển khai độ ưu tiên về việc sử dụng một nhóm độ ưu tiên.

Ở đây, toán tử đó là ^^ và kiểu là infix (binary). Tính kết hợp là right; nói cách khác, nó có nghĩa là độ ưu tiên toán tử ^^ sẽ đánh giá biểu thức từ phải sang trái.

Không có quyền ưu tiên tiêu chuẫn được xác định trước cho các hoạt động theo cấp số nhân trong Swift. Theo thứ tự chuẫn cho các phép toán đại số, phép mũ nên được tính toán trước phép nhân/chia. Do đó bạn sẽ cần tạo một custom độ ưu tiên mà đặt nó cao hơn phép nhân.

Đây là định nghĩa:

Triển khai theo đoạn code sau:

Chú ý rằng bởi vì đoạn code không quan tâm đến việc tràn bộ nhớ, nếu phép toán tạo ra kết quả mà biến Int không thể chứa được, ví dụ như giá trị lớn hơn Int.max, một lỗi sẽ xảy ra trong thời gian chạy.

Câu hỏi #3

Xem xét đoạn code sau định nghĩa Pizza là một struct và Pizzeria là một protocol với một extension bao gồm triển khai mặc định cho makeMargherita():

Bạn bây giờ sẽ định nghĩa nhà hàng Lombardi’s như sau:

Đoạn code bên dưới tạo ra 2 instance của Lombardi’s. Instance nào trong số 2 instance đó sẽ tạo ra một margherita với basil?

Cả hai. Protocol Pizzeria định nghĩa phương thức makeMargherita() và cung cấp một triển khai mặc định. Sự triển khai Lombardis ghi đè lên phương thức mặc định. Bởi vì bạn định nghĩa phương thức trong protocol trong cả hai trường hợp, bạn sẽ gọi đến đúng triển khai trong thời gian chạy.

Sẽ là gì nếu protocol không định nghĩa phương thức makeMargherita() nhưng extension vẫn cung cấp triển khai mặc định, như thế này?

Ở đây, chỉ lombardis2 sẽ tạo ra pizza với basil, trong khi lombardis1 sẽ tạo ra pizza không có nó, bởi vì nó sẽ sử dụng phương thức được định nghĩa trong extension.

Câu hỏi #4

Đoạn code dưới đây có xuất hiện một lỗi trong thời gian biên dịch. Bạn có thể tìm ra nó và giải thích tại sao nó xảy ra?

Bạn có thể fix nó bằng những các gì?

Gợi ý: Có ba cách để sửa lỗi.

Block code phần else của một biểu thức guard yêu cầu một lối kết thúc, bằng việc sử dụng return, ném ra một exception hoặc gọi một @noreturn. Cách dễ nhất là thêm biểu thức return.

Đây là phiên bản ném ra một exception.

Cuối cùng, đây là triển khai việc gọi fatalError(), đây là một hàm @noreturn.

Các Câu Hỏi Nói Nâng Cao

Câu hỏi #1

Các closure là kiểu value hay reference type?

Các closure là kiểu reference type. Nếu bạn gán một closure cho một viến và bạn copy biến đó cho một biến khác, bạn đồng thời copy tham chiếu đến cùng một closure và capture list của nó.

Câu hỏi #2

Bạn sử dụng kiểu UInt để lưu trữ một số nguyên không dấu. Nó triển khai hàm khởi tạo sau để chuyển từ một số nguyên có dấu:

Tuy nhiên, đoạn code sau tạo ra một exception lỗi trong thời gian biên dịch nếu bạn cung cấp một giá trị âm:

Một số nguyên không dấu được định nghĩa là không thể âm. Tuy nhiên, có thể sử dụng vùng nhớ đại diện của một số âm để chuyển thành một số nguyên không dấu. Làm thế nào bạn có thể chuyển một số âm kiểu Int thành một UInt trong khi đang giữ đại diện vùng nhớ của nó?

Có một hàm khởi tạo cho điều đó:

thực hiện lời gọi với nó:

Câu hỏi #3

Bạn có thể mô tả một vòng tham chiếu trong Swift? Làm thế nào để giải quyết nó?

Mọt vòng tham chiếu xảy ra khi hai instance giữ tham chiếu mạnh với nhau, gây nên memory leak bởi vì sẽ không có instance nào được deallocated. Lý do là vì bạn không thể deallocate một instance miễn là vẫn còn tham chiếu mạnh đến nó, nhưng mỗi instance giữ instance kia tồn tại bởi vì tham chiếu mạnh của nó.

Bạn sẽ giải quyết vấn đề bằng việc bẽ gãy tham chiếu vòng tròn mạnh bằng cách thay thế một trong các tham chiếu mạnh với một tham chiếu yếu sử dụng weak hoặc unowned.

Câu hỏi #4

Swift cho phép tạo ra các enum đệ quy. Đây là ví dụ một enum với một case Node lấy hai kiểu giá trị liên kết, TList:

Nó trả về một lỗi biên dịch. Từ khóa nào đang bị thiếu?

Từ khóa indirect cho phép một case enum đệ quy kiểu như vậy:

Tham khảo: https://www.raywenderlich.com/762435-swift-interview-questions-and-answers

--

--