소피it블로그

[Swift 공식문서] Structures and Classes 정리 본문

개발_iOS/스위프트

[Swift 공식문서] Structures and Classes 정리

sophie_l 2022. 5. 24. 10:04

https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

 

Structures and Classes — The Swift Programming Language (Swift 5.6)

Structures and Classes Structures and classes are general-purpose, flexible constructs that become the building blocks of your program’s code. You define properties and methods to add functionality to your structures and classes using the same syntax you

docs.swift.org

구조체와 클래스는 프로그램의 코드를 구성하는 단위가 되는 다목적의 유연한 부품이다. 상수, 변수, 그리고 함수를 정의하는 것과 같은 문법으로 구조체와 클래스에 프라퍼티와 메서드를 정의함으로써 기능성을 부여할 수 있다.

다른 프로그래밍 언어와 다르게 스위프트는 커스텀한 구조체와 클래스에 별개의 인터페이스와 implementation 파일을 만들도록 강제하지 않는다. 스위프트에서는 하나의 파일 내에 구조체나 클래스를 정의하면 해당 구조체나 클래스의 외부적인 인터페이스가 자동적으로 생성되어 다른 코드가 이를 사용할 수 있다.

 

1. 구조체와 클래스의 비교

 

스위프트에서 구조체와 클래스는 많은 공통점이 있다.

 

  • 값을 저장하기 위해 프라퍼티를 정의할 수 있다.
  • 기능성을 제공하기 위해 메서드를 정의할 수 있다.
  • subscript 문법을 사용해서 값에 접근하기 위해 subscript를 정의할 수 있다.
  • 초기 상태를 설정하기 위한 이니셜라이저를 정의할 수 있다.
  • 디폴트 구현을 넘어서는 기능성 확장을 위해 확장될 수 있다.
  • 특정한 종류의 표준 기능을 제공하기 위해 프로토콜을 따를 수 있다.

한편, 클래스는 구조체에는 없는 추가적인 기능을 가진다.

 

  • 클래스는 상속을 통해 다른 클래스의 특성들을 상속해올 수 있다.
  • 런타임 중에 클래스 인스턴스의 타입을 체크하고 해석할 수 있게 해주는 타입 캐스팅이 존재한다.
  • 클래스의 인스턴스가 할당했던 자원을 다시 풀어줄 수 있도록 하는 deinitializer가 존재한다.
  • 레퍼런스 카운팅이 있어 클래스 인스턴스는 하나 이상의 레퍼런스를 가질 수 있다.

클래스가 지원하는 추가적인 기능들에는 더  큰 복잡성이라는 대가가 따라온다. 일반적인 가이드라인을 주자면 구조체가 더 이해하기 쉽기 때문에 구조체를 쓰는 것을 추천한다. 클래스는 적절한 경우 혹은 필요한 경우에만 쓰도록 하라. 즉 실전에서 직접 정의해서 사용할 커스텀 데이터 타입이 거의 구조체와 enumeration이 될 것이라는 뜻이다.

 

(1) Definition Syntax

 

구조체와 클래스는 정의할 때 비슷한 문법을 가진다. 구조체는 struct 키워드를, 클래스는 class 키워드를 사용한다. 둘 다 전체 정의를 한 쌍의 중괄호 안에 적는다.

구조체나 클래스를 선언하는 것은 새로운 스위프트 타입을 선언하는 것이다. 따라서 UpperCamelCase로 이름을 지어주어라. 프라퍼티와 메서드는 타입명과의 구별을 위해 lowerCamelCase 이름을 붙여라.

struct Resolution {
    var width = 0
    var height = 0
}
class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

새로 정의된 구조체 Resolution은 width와 height라는 두 개의 stored properties가 있다. Stored 프라퍼티는 구조체나 클래스의 일부분으로 저장된 상수나 변수를 의미한다. 이 두 개의 프라퍼티는 초깃값으로 0을 갖도록 세팅되었기 때문에 Int 타입으로 추론된다.

또한 새로 정의된 클래스 VideoMode는 4개의 stored 프라퍼티를 갖는다. 첫 번째인 resolution은 Resolution 구조체의 인스턴스로써 초기화되며, 타입은 Resolution 타입으로 추론된다. 다른 세 개의 프라퍼티들도 각각 false, 0.0, 옵셔널 스트링 값으로 초기화된다. name 프라퍼티는 옵셔널이기 때문에 자동적으로 nil값을 디폴트로 갖게 된다.

 

(2) 구조체와 클래스의 인스턴스

 

구조체 Resolution과 클래스 VideoMode의 정의는 각각이 어떤 모습일지만 묘사할 뿐, 실제 구체적인 resolution이나 video mode를 묘사하지는 않는다. 그걸 위해서는 구조체나 클래스의 인스턴스를 생성해야 한다.

구조체와 클래스의 인스턴스를 생성하는 문법은 비슷하다.

let someResolution = Resolution()
let someVideoMode = VideoMode()

구조체와 클래스는 둘 다 새로운 인스턴스를 위해 이니셜라이저 구문을 사용한다. 이니셜라이저 구문의 가장 간단한 형태는 Resolution()이나 VideoMode()처럼 클래스나 구조체의 타입명 뒤에 빈 괄호를 쳐주는 것이다. 그렇게 해주면 해당 클래스나 구조체의 새로운 인스턴스가 생성되며, 프라퍼티는 디폴트 값으로 초기화된다.

 

(3) 프라퍼티에 접근하기

 

도트 구문을 이용하여 인스턴스의 프라퍼티에 접근할 수 있다. 도트 구문에서는 인스턴스 명 뒤에 공백 없이 온점(.)을 찍은 후 프라퍼티 명을 적어준다.

print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0"

// 서브프라퍼티에도 접근할 수 있다.
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0"

// 변수 프라퍼티에 새로운 값을 할당해주는 용도로도 사용할 수 있다.
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is now 1280"

(4) Memberwise Initializers for Structure Types

 

모든 구조체는 자동적으로 생성된 멤버와이즈 이니셜라이저가 있는데, 이를 통해 새로운 구조체 인스턴스의 멤버 프라퍼티를 초기화해줄 수 있다. 새로운 인스턴스의 프라퍼티의 초깃값은 이름을 통해 멤버와이즈 이니셜라이저에 전달될 수 있다.

let vga = Resolution(width: 640, height: 480)

구조체와 다르게 클래스의 인스턴스는 디폴트 멤버와이즈 이니셜라이저를 받지 않는다.

 

2. 구조체와 Enumeration은 Value Type이다

 

밸류 타입은 상수나 변수에 할당되거나 함수에 전달될 때 값이 복제되는 타입을 의미한다.

앞에서 살펴본 타입들은 대부분 밸류 타입들이었고, 실제로도 정수, 실수, 불리언, 문자열, 배열, 그리고 딕셔너리 등 스위프트의 기본 타입들은 전부 밸류 타입이며 구조체로써 구현된다.

스위프트에 모든 구조체와 enumeration은 밸류 타입이다. 즉, 생성되는 모든 구조체와 enumeration 클래스, 그리고 이들이 프라퍼티로써 갖는 밸류 타입은 모두 코드 내부에서 전달될 때 복제된다는 것이다.

배열, 딕셔너리, 문자열 등 표준 라이브러리에 의해 정의된 콜렉션 타입은 복제를 수행할 때의 비용을 줄이는 최적화 기법을 사용한다. 이러한 콜렉션 타입은 즉각 복제본을 만들기보다는, 오리지널 인스턴스와 복제본 사이의 공간을 공유한다. 만일 콜렉션의 복제본 중 하나가 수정된다면 수정 직전에 요소들이 카피된다.

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

Resolution은 구조체이기 때문에 현재의 인스턴스hd의 복제본이 만들어지며, 이 새로운 복제본이 cinema에 할당된다. hd와 cinema는 현재 동일한 width와 height를 가지고 있지만, 이 둘은 실제로는 완전히 다른 별개의 인스턴스이다.

cinema.width = 2048

// cinema의 width
print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide"

// 오리지널 인스턴스인 hd의 width
print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide"

cinema에 hd의 현재값이 주어졌을 때 hd 안에 저장되어있던 값들이 새로운 cinema 인스턴스로 복제된다. 결과적으로는 같은 숫자를 포함하는 서로 다른 별개의 인스턴스이다. 두 개가 별개의 인스턴스이기 때문에 cinema의 width를 2048로 변형하는 것은 hd에 저장된 width를 변경시키지 않는다.

이는 enumeration에서도 동일하게 적용된다.

enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() {
        self = .north
    }
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// Prints "The current direction is north"
// Prints "The remembered direction is west"

3. 클래스는 Reference 타입이다

 

밸류 타입과 다르게 레퍼런스 타입은 변수나 상수에 할당되거나 함수에 전달될 때 복제되지 않는다. 복제본 대신 이미 존재하는 같은 인스턴스에 대한 참조가 사용된다.

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

tenEighty라는 새로운 상수는 VideoMode 클래스의 새로운 인스턴스를 참조하도록 선언되었다.  그 후 tenEighty는 alsoTenEighty라는 새로운 상수로 다시 할당되고, alsoTenEighty의 frame rate가 수정된다.

클래스는 레퍼런스 타입이기 때문에 tenEighty와 alsoTenEighty는 실제로 같은 VideoMode 인스턴스를 참조한다. 결과적으로 둘은 하나의 인스턴스를 나타내는 두 가지 다른 이름일 뿐이다.

tenEighty의 frameRate 프라퍼티를 확인해보면 새로운 프레임 비율인 30.0이 되었다는 것을 알 수 있다.

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0"

레퍼런스 타입이 더 이해하기 쉽지 않은 개념이다. 만일 tenEighty와 alsoTenEighty가 코드 내에서 멀리 떨어져 있었다면 비디오 모드가 바뀌는 과정을 찾기가 어려울 것이다. tenEighty를 사용할 때면 늘 alsoTenEighty에 대해서도 고려해봐야 하고, 그 반대도 마찬가지이다. 반면 밸류 타입은 같은 값을 사용하는 코드들이 전부 소스 파일 내에서  가깝게 위치해있기 때문에 이해하기가 수월하다.

tenEighty와 alsoTenEighty는 변수가 아닌 상수로 선언되었다. 그런데도 tenEighty.frameRate와 alsoTenEighty.frameRate를 여전히 바꿀 수 있다. 이는 tenEighty와 alsoTenEighty라는 상수의 값 자체는 바뀌지 않기 때문이다. tenEighty와 alsoTenEighty는 VideoMode 인스턴스 자체를 저장하는 것이 아니다. 대신 VideoMode 인스턴스를 뒤에서 참조하고 있을 뿐이다. VideoMode의 frameRate 프라퍼티가 바뀐 것이지 VideoMode의 상수 참조의 값이 바뀐 것이 아니다.

 

(1) Identity Operators

 

클래스는 레퍼런스 타입이기 때문에 클래스의 인스턴스 하나를 여러 개의 상수나 변수가 참조할 수 있다. 반면 구조체나 enumeration은 상수나 변수에 할당되거나 함수에 전달될 때 항상 복사가 되기 때문에 이와는 다르다.

때로는 두 개의 상수나 변수가 완전히 같은 클래스 인스턴스를 가리키는지 확인해보면 좋을 때가 있다. 이를 위해 스위프트는 두 개의 identity operators를 제공한다.

 

  • Identical to (===)
  • Not identical to (!==)
if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."

identical to(===)와 equal to(==)는 다르다는 것에 주의해야 한다. ===는 두 개의 상수 또는 변수가 똑같은 클래스 인스턴스를 참조하고 있다는 것을 의미하는 반면, ==는 두 개의 인스턴스가 값에 있어서 같거나 동등하다고 여겨진다는 의미이다.

 

(2) Pointers

 

특정한 레퍼런스 타입을 참조하는 스위프트의 상수나 변수는 C의 포인터와 비슷하지만, 메모리를 가리키는 직접적인 포인터가 아니며 레퍼런스를 생성하고 있다는 것을 나타내기 위해 *를 써줄 필요가 없다. 대신 이 레퍼런스들은 스위프트의 다른 상수나 변수처럼 정의된다. 표준 라이브러리는 포인터를 직접적으로 사용해야 할 때 쓸 수 있는 포인터와 버퍼 타입을 제공한다.