대부분의 Swift 개발자는 자신이 저지르는 실수를 모릅니다
게시 됨: 2022-03-11Objective-C를 배경으로 해서 처음에는 Swift가 저를 막고 있는 것 같았습니다. Swift는 때때로 화를 돋우던 강력한 유형의 특성 때문에 내가 발전하는 것을 허용하지 않았습니다.
Objective-C와 달리 Swift는 컴파일 시간에 많은 요구 사항을 적용합니다. id
유형 및 암시적 변환과 같이 Objective-C에서 완화된 사항은 Swift의 것이 아닙니다. Int
와 Double
이 있고 더하고 싶다면 명시적으로 단일 유형으로 변환해야 합니다.
또한 옵셔널은 언어의 기본 요소로 간단한 개념임에도 익숙해지는데 시간이 걸립니다.
처음에는 모든 것을 강제로 풀기를 원할 수 있지만 결국 충돌로 이어질 것입니다. 언어에 익숙해지면 컴파일 시간에 많은 실수가 포착되기 때문에 런타임 오류가 거의 발생하지 않는다는 점을 좋아하게 됩니다.
대부분의 Swift 프로그래머는 Objective-C에 대한 상당한 이전 경험이 있으며, 이는 무엇보다도 다른 언어에서 익숙한 것과 동일한 방식을 사용하여 Swift 코드를 작성하게 할 수 있습니다. 그리고 그것은 몇 가지 나쁜 실수를 일으킬 수 있습니다.
이 기사에서는 Swift 개발자가 저지르는 가장 일반적인 실수와 이를 피하는 방법에 대해 간략히 설명합니다.
1. 강제 풀기 옵션
선택적 유형의 변수(예: String?
)는 값을 보유하거나 보유하지 않을 수 있습니다. 값을 보유하지 않으면 nil
과 같습니다. 옵셔널의 값을 얻으려면 먼저 그것들을 풀어야 하고 두 가지 다른 방법으로 만들 수 있습니다.
한 가지 방법은 if let
또는 guard let
을 사용하는 선택적 바인딩입니다.
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
두 번째는 !
연산자를 사용하거나 암시적으로 래핑되지 않은 선택적 유형(예: String!
)을 사용합니다. 옵셔널이 nil
인 경우 unwrap을 강제 실행하면 런타임 오류가 발생하고 애플리케이션이 종료됩니다. 또한 암시적으로 래핑되지 않은 옵셔널의 값에 액세스하려고 하면 동일한 결과가 발생합니다.
때때로 클래스/구조체 초기화 프로그램에서 초기화할 수 없는(또는 원하지 않는) 변수가 있습니다. 따라서 선택 사항으로 선언해야 합니다. 어떤 경우에는 코드의 특정 부분에서 nil
이 되지 않을 것이라고 가정하므로 항상 옵셔널 바인딩을 수행해야 하는 것보다 쉽기 때문에 강제로 언래핑하거나 암시적으로 언래핑된 옵셔널로 선언합니다. 이 작업은 주의해서 수행해야 합니다.
이것은 펜촉이나 스토리보드에서 객체를 참조하는 변수인 IBOutlet
으로 작업하는 것과 유사합니다. 부모 객체의 초기화(보통 뷰 컨트롤러 또는 사용자 정의 UIView
) 시 초기화되지 않지만 viewDidLoad
(뷰 컨트롤러에서) 또는 awakeFromNib
(뷰에서)가 호출될 때 nil
이 되지 않을 것임을 확신할 수 있습니다. 안전하게 액세스할 수 있습니다.
일반적으로 가장 좋은 방법은 강제로 unwrap을 피하고 암시적으로 unwrapped 옵션을 사용하는 것입니다. 항상 옵셔널이 nil
일 수 있다고 생각하고 옵셔널 바인딩을 사용하거나 unwrap을 강제 실행하기 전에 nil
이 아닌지 확인하거나 암시적으로 언래핑된 옵셔널의 경우 변수에 액세스하여 적절하게 처리합니다.
2. 강한 참조 주기의 함정을 알지 못함
강한 참조 주기는 한 쌍의 객체가 서로에 대한 강한 참조를 유지할 때 존재합니다. Objective-C에 동일한 문제가 있고 노련한 Objective-C 개발자가 이를 적절히 관리해야 하기 때문에 이것은 Swift에 새로운 것이 아닙니다. 강력한 참조와 참조 대상에 주의를 기울이는 것이 중요합니다. Swift 문서에는 이 주제 전용 섹션이 있습니다.
클로저를 사용할 때 참조를 관리하는 것이 특히 중요합니다. 기본적으로 클로저(또는 블록)는 내부에서 참조되는 모든 객체에 대한 강력한 참조를 유지합니다. 이러한 객체 중 하나에 클로저 자체에 대한 강력한 참조가 있는 경우 강력한 참조 주기가 있습니다. 참조 캡처 방법을 적절하게 관리하려면 캡처 목록 을 사용해야 합니다.
블록에 의해 캡처된 인스턴스가 블록이 호출되기 전에 할당 해제될 가능성이 있는 경우 약한 참조 로 캡처해야 하며 이는 nil
일 수 있으므로 선택 사항입니다. 이제 캡처된 인스턴스가 블록의 수명 동안 할당 해제되지 않을 것이라고 확신하는 경우 소유되지 않은 참조 로 캡처할 수 있습니다. weak
대신 unowned
것을 사용하는 이점은 참조가 선택 사항이 아니며 래핑을 해제할 필요 없이 값을 직접 사용할 수 있다는 것입니다.
Xcode Playground에서 실행할 수 있는 다음 예제에서 Container
클래스에는 배열이 있고 배열이 변경될 때마다 호출되는 선택적 클로저가 있습니다(속성 관찰자를 사용하여 그렇게 함). Whatever
클래스는 Container
인스턴스를 가지고 있으며, 초기화에서 arrayDidChange
에 클로저를 할당하고 이 클로저는 self
를 참조하므로 Whatever
인스턴스와 클로저 사이에 강력한 관계가 생성됩니다.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
이 예제를 실행하면 인쇄되지 않는 항목을 초기화한다는 deinit whatever
알 수 있습니다. 이는 w
인스턴스가 메모리에서 할당 해제되지 않는다는 것을 의미합니다. 이 문제를 해결하려면 self
를 강력하게 캡처하지 않도록 캡처 목록을 사용해야 합니다.
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
이 경우 클로저 수명 동안 self
가 nil
이 되지 않을 것이기 때문에 unowned
를 사용할 수 있습니다.
거의 항상 캡처 목록을 사용하여 참조 주기를 피하는 것이 좋습니다. 그러면 메모리 누수가 줄어들고 결국에는 더 안전한 코드가 됩니다.
3. 어디서나 self
사용하기
Objective-C와 달리 Swift에서는 메서드 내에서 클래스 또는 구조체의 속성에 액세스하기 위해 self
를 사용할 필요가 없습니다. self
를 캡처해야 하기 때문에 클로저에서만 그렇게 하면 됩니다. 필요하지 않은 곳에서 self
를 사용하는 것은 정확히 실수가 아니며 잘 작동하며 오류나 경고도 없습니다. 그러나 왜 필요한 것보다 더 많은 코드를 작성해야 합니까? 또한 코드를 일관성 있게 유지하는 것이 중요합니다.
4. 자신의 유형을 모르는 경우
Swift는 값 유형 과 참조 유형 을 사용합니다. 또한 값 유형의 인스턴스는 참조 유형의 인스턴스와 약간 다른 동작을 나타냅니다. 각 인스턴스가 어떤 범주에 속하는지 모르면 코드 동작에 대한 잘못된 기대가 발생합니다.
대부분의 객체 지향 언어에서 클래스의 인스턴스를 생성하여 다른 인스턴스에 전달하고 메서드에 대한 인수로 전달할 때 우리는 이 인스턴스가 모든 곳에서 동일할 것으로 기대합니다. 이는 변경 사항이 모든 곳에 반영된다는 것을 의미합니다. 사실 우리가 가지고 있는 것은 정확히 동일한 데이터에 대한 참조의 무리일 뿐입니다. 이러한 동작을 보이는 객체는 참조 유형이며, Swift에서는 class
로 선언된 모든 유형이 참조 유형입니다.
다음으로 struct
또는 enum
을 사용하여 선언된 값 유형이 있습니다. 값 형식은 변수에 할당되거나 함수 또는 메서드에 인수로 전달될 때 복사됩니다. 복사한 인스턴스에서 무언가를 변경해도 원본 인스턴스는 수정되지 않습니다. 값 유형은 변경할 수 없습니다. CGPoint
또는 CGSize
와 같은 값 유형 인스턴스의 속성에 새 값을 할당하면 변경 사항과 함께 새 인스턴스가 생성됩니다. 그렇기 때문에 위의 Container
클래스 예제에서와 같이 배열에서 속성 관찰자를 사용하여 변경 사항을 알릴 수 있습니다. 실제로 일어나는 일은 변경 사항으로 새 배열이 생성된다는 것입니다. 속성에 할당된 다음 didSet
이 호출됩니다.
따라서 다루고 있는 객체가 참조 또는 값 유형인지 모르는 경우 코드가 수행할 작업에 대한 예상이 완전히 틀릴 수 있습니다.

5. 열거형의 잠재력을 최대한 활용하지 않기
열거형에 대해 이야기할 때 일반적으로 기본 C 열거형을 생각합니다. 이 열거형은 아래에 있는 정수인 관련 상수의 목록일 뿐입니다. Swift에서 열거형은 훨씬 더 강력합니다. 예를 들어, 각 열거 케이스에 값을 첨부할 수 있습니다. 열거형에는 더 많은 정보와 세부 정보로 각 사례를 강화하는 데 사용할 수 있는 메서드 및 읽기 전용/계산 속성도 있습니다.
열거형에 대한 공식 문서는 매우 직관적이며 오류 처리 문서는 Swift에서 열거형의 추가 기능에 대한 몇 가지 사용 사례를 제공합니다. 또한 Swift에서 열거형에 대한 광범위한 탐색을 확인하여 열거형으로 할 수 있는 거의 모든 것을 배우십시오.
6. 기능적 기능을 사용하지 않음
Swift 표준 라이브러리는 함수형 프로그래밍의 기본이 되는 많은 방법을 제공하며 무엇보다도 map, reduce 및 filter와 같은 한 줄의 코드로 많은 작업을 수행할 수 있도록 합니다.
몇 가지 예를 살펴보겠습니다.
예를 들어 테이블 뷰의 높이를 계산해야 합니다. 다음과 같은 UITableViewCell
하위 클래스가 있다고 가정합니다.
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
모델 인스턴스의 배열이 있습니다. modelArray
; 한 줄의 코드로 테이블 뷰의 높이를 계산할 수 있습니다.
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
map
는 각 셀의 높이를 포함하는 CGFloat
배열을 출력하고 reduce
는 그것들을 합산합니다.
배열에서 요소를 제거하려면 다음을 수행해야 할 수 있습니다.
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
이 예제는 각 항목에 대해 indexOf
를 호출하기 때문에 우아하지도 않고 효율적이지도 않습니다. 다음 예를 고려하십시오.
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
이제 코드가 더 효율적이지만 filter
를 사용하여 더 개선할 수 있습니다.
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
다음 예제는 특정 사각형과 교차하는 프레임과 같은 특정 기준을 충족하는 UIView
의 모든 하위 보기를 제거하는 방법을 보여줍니다. 다음과 같이 사용할 수 있습니다.
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
하지만 멋진 필터링 및 변환을 생성하기 위해 이러한 메서드에 대한 몇 가지 호출을 연결하고 싶은 유혹을 받을 수 있으므로 주의해야 합니다. 이 작업은 결국 읽을 수 없는 스파게티 코드 한 줄로 끝날 수 있습니다.
7. 프로토콜 지향 프로그래밍을 시도하지 않고 편안함 영역에 머물기
Swift는 WWDC Protocol-Oriented Programming in Swift 세션에서 언급했듯이 최초의 프로토콜 지향 프로그래밍 언어 라고 주장됩니다. 기본적으로 이는 프로토콜을 기반으로 프로그램을 모델링하고 프로토콜을 준수하고 확장함으로써 유형에 동작을 추가할 수 있음을 의미합니다. 예를 들어 Shape
프로토콜이 있는 경우 CollectionType
( Array
, Set
, Dictionary
와 같은 유형을 준수함)을 확장하고 교차점을 설명하는 총 면적을 계산하는 메서드를 추가할 수 있습니다.
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
where Generator.Element: Shape
는 확장의 메서드가 Shape
를 준수하는 유형의 요소를 포함하는 CollectionType
을 준수하는 유형의 인스턴스에서만 사용할 수 있음을 나타내는 제약 조건입니다. 예를 들어 이러한 메서드는 Array<Shape>
의 인스턴스에서 호출할 수 있지만 Array<String>
의 인스턴스에서는 호출할 수 없습니다. Shape
프로토콜을 준수하는 Polygon
클래스가 있는 경우 해당 메서드는 Array<Polygon>
인스턴스에도 사용할 수 있습니다.
프로토콜 확장을 사용하면 프로토콜에 선언된 메소드에 기본 구현을 제공할 수 있습니다. 그러면 해당 유형(클래스, 구조체 또는 열거형)을 변경할 필요 없이 해당 프로토콜을 준수하는 모든 유형에서 사용할 수 있습니다. 이것은 Swift 표준 라이브러리 전체에서 광범위하게 수행됩니다. 예를 들어 map
및 reduce
는 CollectionType
확장에 정의되어 있으며 이 동일한 구현은 추가 코드 없이 Array
및 Dictionary
와 같은 유형에서 공유됩니다.
이 동작은 Ruby 또는 Python과 같은 다른 언어의 믹스 인과 유사합니다. 기본 메소드 구현으로 프로토콜을 준수하기만 하면 유형에 기능을 추가할 수 있습니다.
프로토콜 지향 프로그래밍은 언뜻 보기에는 매우 어색하고 그다지 유용하지 않아 보일 수 있으므로 무시하고 시도조차 하지 않을 수 있습니다. 이 게시물은 실제 응용 프로그램에서 프로토콜 지향 프로그래밍을 사용하는 방법을 잘 이해합니다.
우리가 배웠듯이, Swift는 장난감 언어가 아닙니다
Swift는 처음에 많은 회의론을 받았습니다. 사람들은 Apple이 Objective-C를 어린이용 장난감 언어나 프로그래머가 아닌 언어로 대체할 것이라고 생각하는 것 같았습니다. 그러나 Swift는 프로그래밍을 매우 즐겁게 만드는 진지하고 강력한 언어임이 입증되었습니다. 타이핑이 강해서 실수하기 어렵고, 그렇기 때문에 언어로 할 수 있는 실수를 나열하기가 어렵습니다.
Swift에 익숙해지고 Objective-C로 돌아가면 차이점을 알 수 있습니다. Swift가 제공하는 좋은 기능을 놓치고 동일한 효과를 얻으려면 Objective-C로 지루한 코드를 작성해야 합니다. 다른 경우에는 Swift가 컴파일 중에 포착했을 런타임 오류에 직면하게 됩니다. 이것은 Apple 프로그래머를 위한 훌륭한 업그레이드이며 언어가 성숙해짐에 따라 아직 더 많은 것이 있습니다.