Apple Swift Programming Language
translation for KOREAN
translation for KOREAN
This is a The Swift Programming Language guide documentation translation project.
Disclaimer : It is not an official translation project, and it is an independent translation project; our activities and the opinions expressed on this WebSite should in no way be linked to Apple, the corporation.
This project never be used for commercial purposes. By translating "The Swift Programming Language" we want to help a lot of korean learners who may otherwise struggle to understand it.
번역 된 Swift문서는 http://lean-tra.github.io/Swift-Korean 를 통해 보실 수있습니다. 현재 무리없이 읽을 수 있는 수준으로만 번역되었습니다. 앞으로 더 나은 퀄리티를 위하여 별도의 공지 없이 계속해서 업데이트 될 예정입니다.
이 프로젝트는 애플의 새로운 프로그래밍 언어인 Swift 문서 번역 프로젝트 입니다. 이 프로젝트는 공식적인 번역 프로젝트가 아니며, 이 곳에서 나오는 의견 및 번역은 애플사의 입장과 전혀 무관합니다. 이 번역 프로젝트는 절대로 상업적인 목적으로 사용되지 않으며, 사용할 수도 없습니다. 번역물의 이용에 대한 자세한 사항은 링크를 참조하세요.
Translator : FlashMaestro (masterofflash@nate.com)
Swift는 C 언어 그리고 Objective-C 언어의 좋은 점들을 취합한 것을 기반으로 C 언어 호환성에 대한 제약 없이 iOS와 OS X 앱을 개발하기 위한 언어입니다. Swift는 안전한 프로그래밍 패턴을 채용했고 프로그래밍을 더 쉽고, 유연하고, 재미있게 만들어주는 최신 특징을 더했습니다. 성숙하고 많은 사랑을 받고 있는 코코아, 코코아 터치 프레임워크를 기반으로 둔 Swift의 이런 훌륭함은 소프트웨어 개발 방법을 재고해볼 기회를 제공합니다. 알겠습니다.
Swift는 오랜 기간에 걸쳐 만들어져 왔습니다. 애플은 Swift를 위해 현존하는 진보한 컴파일러, 디버거, 프레임워크 기반의 토대를 만들었습니다. 우리는 ARC(Auto Reference Counting)로 메모리 관리를 단순화 했습니다. Foundation(Apple Foundation Framework)과 코코아의 견고한 기초를 기반으로 만들어진 우리의 프레임워크 스텍은 최신화와 표준화의 결과물이죠. Objective-C는 블록 방식 코딩, 문자집합, 모듈, 혼란없는 최신 언어 기술 프레임워크 적용 가능 등을 지원하도록 발전해왔습니다. 이러한 기초작업 덕분에 애플 소프트웨어 개발의 미래를 위한 새로운 언어를 소개할 수 있게 되었습니다.
Swift는 Objective-C 개발자들에게 매우 친숙한 느낌을 줍니다. Swift가 개발자가 읽기 쉽도록 Objective-C의 파라미터 명명법과 동적 객체 모델의 성능을 적용했기 때문입니다. 이를 통해 기존 코코아 프레임워크에 매끄럽게 접근할 수 있고 Objective-C와 혼합해서 사용할 수도 있습니다. 이러한 공통점을 기반으로 두고, Swift는 많은 새로운 특징들을 도입했고 절차지향 프로그래밍과 객체지향 프로그래밍 요소들을 통합했습니다.
Swift는 프로그래밍 입문자에게 친화적입니다. Swift는 스크립트 언어만큼이나 표현하기 쉽고 즐거운데다가 산업품질에 적합한 시스템 프로그래밍 언어입니다. Swift는 프로그래머들이 사소한 테스트 때문에 코드를 빌드하고 실행하는 과정에 시간을 낭비하는 일 없이 즉시 코드를 테스트하고 결과를 볼 수 있도록 하는 혁신적인 요소인 playgrounds를 지원합니다.
Swift는 폭넓은 애플 엔지니어링 문화로 부터 나온 지혜와 뜻에 부합하기 위해 최신 언어 중 최고의 것들을 갖추도록 했습니다. 성능과 개발편의성을 타협할 필요가 없을 만큼 컴파일러는 성능 향상을 목적으로 최적화했고, 언어는 개발편의성을 고려해 최적화 했습니다. Swift는 'hello, world'에서 시작해 전체 운영체제로 확장할 수 있게 디자인 되었습니다. 이 모든것은 결국 애플과 개발자들이 Swift에 투자할 가치가 있다고 생각하게 합니다.
Swift는 iOS와 OS X 앱을 만드는 것은 물론, 계속 새로운 기능을 추가하고 개선하는 위한 환상적인 방법입니다. Swift를 향한 우리의 목표는 야심찹니다. 우리는 여러분이 Swift로 무언가 만드는 것을 빨리 보고 싶습니다.
Translator : FlashMaestro (masterofflash@nate.com)
전통적으로 새로운 언어를 배울 때 첫번째 프로그램은 화면에 "Hello, world"란 구문을 출력해 보는 것입니다. Swift에서는 코드 한 줄로 이를 구현할 수 있습니다.
println("Hello, world")
C나 Objective-C에서 코드를 작성해본적이 있다면 이런 문법이 매우 익숙할 것입니다. Swift에서는 이 한줄의 코드가 완전한 프로그램입니다. 입/출력이나 문자열을 다루기 위한 함수들 같은 기능들을 사용하기 위해 분리된 라이브러리를 볼러올(Import) 필요가 없습니다. 전역 범위(Global scope)에 해당하는 코드는 프로그램의 진입점(entry point)로 사용되기 때문에, main 함수는 필요하지 않습니다. 또한 모든 문장끝에 세미콜론을 쓸 필요도 없습니다.
이번 장에서는 다양한 프로그래밍 과제를 어떻게 완수해 나가는지 보여주면서 Swift로 어떻게 코드를 작성해야하는지에 대한 충분한 정보를 제공할 것입니다. 몇몇가지를 이해하지 못한다고 해서 걱정하지 마세요. 이번 장에서 소개되는 모든 것들은 책의 뒷 부분에서 자세히 설명할 것입니다.
노트 이번 장을 확실하게 이해하려면 Xcode 6의 Playground를 열어보세요. Playground는 코드를 수정하는 즉기 결과를 볼 수 있게 해줄 것입니다.
let을 사용하면 상수를 만들고 var를 사용하면 변수를 만들 수 있습니다. 상수는 컴파일할 때 값을 알 필요가 없습니다. 그러나 한 번만 값을 할당할 수 있습니다. 이는 상수에 한 번만 값을 할당한 다음, 해당 상수 이름을 여러곳에 사용할 수 있다는 것을 의미합니다.
var myVariable =42
myVariable = 50
let myConstant = 42
상수나 변수 값 모두에 여러분이 할당하고 싶은 값의 타입(type)을 지정할 수 있습니다. 그러나 항상 타입을 명시해야만 하는 것은 아닙니다. 여러분이 상수나 변수를 만들 때 할당한 값을 통해 컴파일러는 해당 값의 타입을 추측합니다. 위의 예를 보면, myVariable이 정수 타입 값으로 초기화되었기 때문에 컴파일러는 정수 타입이라고 추측합니다.
만약 초기값이 충분한 정보를 제공하지 못한 경우(혹은 초기값이 없는 경우) 특정한 타입을 변수명 뒤에 콜론으로 분리하여 써줘야 합니다.
let inplicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70
실습 명시적으로 Float 타입인 상수를 만들고 4라는 값을 할당해보자.
할당된 값은 절대 다른 타입의 값으로 암시적인 변환을 할 수 없습니다. 만약 다른 타입의 값으로 변화해야 한다면 원하는 형태의 인스턴스로 만들어 타입을 명시해야 합니다.
let label = "The width is "
let width = 94
let widthLabel = label + String(width)
실습 마지막 줄에 명시한 String 타입 변환 부분을 제거해보자. 어떤 에러가 발생하는가?
문자열 안에 값들을 쉽게 포함하는 방법도 있습니다. 괄호 안에 값을 쓰고 괄호 앞에 백슬래시()를 쓰면 됩니다. 예를 들면
let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."
과 같습니다.
실습
\()
를 이용해 문자열 안에 실수형 계산을 포함하도록 해보고, 인사말 안에 누군가의 이름을 넣어보자.
배열(array)과 딕셔너리(dictionary)는 대괄호([])를 이용해 만들 수 있습니다. 그리고 대괄호 안에 인덱스(index)나 키(key)를 이용해 각각의 요소에 접근할 수 있습니다.
var shoppingList = ["catfish", "water", "tulips", "blue paint"]
shoppingList[1] = "bottle of water"
var occupations = [
"Malcolm":"Captian",
"Kaylee":"Mechanic",
]
occupations["Jayne"] = "Public Relations"
빈 배열이나 딕셔너리를 만들려면, 이니셜라이저(initializer)를 사용하면 됩니다.
let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()
타입 정보를 추론할 수 없다면 빈 배열은 []로 빈 딕셔너리는 [:]로 표기할 수 있습니다. 예를 들어 변수에 새로운 값을 할당하거나 함수에 인자(argument)로 전달할 때
shoppingList = [] // Went shopping and bought everything.
와 같이 표현하면 됩니다.
if와 switch를 사용해서 조건문을 만들 수 있고 for-in, for, while, do-while을 이용해서 반복문을 만들 수 있습니다. 조건문과 반복문을 괄호로 감싸는 것은 선택사항입니다. 단, 중괄호로 해당 문 안(body)의 코드를 감싸는 것은 필수입니다.
let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
if score > 50 {
teamScore += 3
} else {
teamScore += 1
}
}
teamScore
if문 안의 조건은 꼭 불리언(Boolean) 표현이어야 합니다. if score {...}라고 표현하면 0과의 비교를 암시하지 않기 때문에 에러가 발생합니다.
빈 값을 가지고 if와 let을 함께 사용하는 것도 가능합니다. 이런 값들은 옵션으로 표현합니다. 옵션 값은 어떤 값을 가지거나 빈 값을 표현하는 nil로 지정하기도 합니다. 값의 타입 뒤에 물음표를 쓰면 옵션 값이라는 것을 나타냅니다.
var optionalString: String? = "Hello"
optionalString == nil
var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
greeting = "Hello, \(name)"
}
실습 optionalName의 값을 nil로 바꿔보자. 어떤 greeting의 값을 받을 수 있는가? optionalName에 할당된 값이 nil일 때 다른 값을 greeting에 할당하도록 else 절을 추가해보자.
만약 옵션 값이 nil이라면 조건문은 거짓(false)이고 중괄호 안에 있는 코드를 실행하지 않고 건너뜁니다. 반대의 경우에는 중괄호 블록 안에서 사용할 수 있도록 let 뒷 부분의 상수에 값이 할당되고 옵션 값으로 사용할 수 있습니다.
switch
문에는 정수 타입 값이나 동등 비교연산 뿐만 아니라 어떤 종류의 데이터든 사용할 수 있고 다양한 비교 연산자들을 사용할 수 있습니다.
let vegetable = "red pepper"
switch vegetable {
case "celery":
let vegetableComment = "Add some raisins and make ants on a log."
case "cucumber", "watercress":
let vegetableComment = "That would make a good tea sandwich."
case let x where x.hasSuffix("pepper"):
let vegetableComment = "Is it a spicy \(x)?"
default:
let vegetableComment = "Everything tastes good in soup."
}
실습 switch문에서 default 부분을 제거해 보자. 어떤 에러가 발생하는가?
switch문은 case문의 조건과 일치할 때 case문 아래에 속한 코드를 실행하 switch문을 빠져나옵니다. 이후에 나오는 case문은 자동적으로 실행하지 않기 때문에 코드 끝에 break 키워드를 명시하지 않아도 됩니다.
for-in 문을 사용하면 각각 키/값 쌍으로 사용할 수 있는 이름들의 쌍을 이용해 딕셔너리에 있는 요소들을 반복 처리할 수 있습니다.
let interestingNumbers = [
"Prime": [2, 3, 5, 7, 11, 13],
"Fibonacci": [1, 1, 2, 3, 5, 8],
"Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (kind, numbers) in interestingNumbers {
for number in numbers {
if number > largest {
largest = number
}
}
}
largest
실습 어떤 숫자가 가장 큰 수로 저장되는지 확인하기 위해 다른 변수를 추가하고, 가장 큰 수로 저장된 숫자가 무엇인지 확인해보라.
이번에는 조건문이 변경될 때까지 코드 블록을 반복 실행하기 위해서 while 문을 사용해 봅시다. 반복문이 적어도 한번은 실행될 수 있도록 보장하려면 조건문을 반복문의 끝에 작성할 수도 있습니다.
var n = 2
while n < 100 {
n = n * 2
}
n
var m = 2
do {
m = m * 2
} while m < 100
m
반복문안에서 ..
을 사용해 인덱스의 범위를 만들거나 명시적으로 초기화, 조건문, 증감식을 사용할 수도 있습니다. 아래 두 가지 반복문은 동일한 작업을 수행합니다.
var firstForLoop = 0
for i in 0..3 {
firstForLoop += i
}
firstForLoop
var secondForLoop = 0
for var i = 0; i < 3; ++i {
secondForLoop += 1
}
secondForLoop
..
을 사용해서 범위를 지정하면 제일 맨 마지막 값은 제외됩니다. 반면에 ...
을 사용하면 양쪽 끝의 값을 모두 범위에 포함하게 됩니다.
func
를 사용해 함수를 선언할 수 있습니다. 함수를 호출할 때 함수의 이름과 괄호안에 인자들을 넣을 수 있습니다. 매개변수의 이름과 분리해서 '->'를 사용해 타입 이름을 표기하면 함수 반환 값의 타입을 지정할 수 있습니다.
func greet(name: String, day: String) -> String {
return "Hello \(name), today is \(day)."
}
greet("Bob", "Tuesday")
실습 매개변수 day를 제거하고 인사말에 '오늘의 특별한 점심'을 포함하도록 매개변수를 추가해보자.
튜플(tuple)을 사용하면 여러개의 값을 반환할 수 있습니다.
func getGasPrices() -> (Double, Double, Double) {
return (3.59, 3.69, 3.79)
}
getGasPrices()
배열을 이용해서 여러 개의 값을 함수의 인자로 받을 수도 있습니다.
func sumOf(numbers: Int...) -> Int {
var sum = 0
for number in numbers {
sum += number
}
return sum
}
sumOf()
sumOf(42, 597, 12)
실습 인자들의 평균값을 계산하는 함수를 만들어보자.
함수는 중첩해서 사용할 수도 있습니다. 중첩(Nested) 함수는 감싸고 있는 함수에서 선언된 변수에 접근할 수 있습니다. 코드가 길어지고 복잡해지는 함수라면 이를 정리하려고 중첩 함수를 사용할 수 있습니다.
func returnFifteen() -> Int {
var y = 10
func add() {
y += 5
}
add()
return y
}
returnFifteen()
함수는 최상위(first-class) 타입입니다. 즉, 어떤 함수가 다른 함수를 반환 값 형태로 반환할 수 있다는 것을 의미합니다.
func makeIncrementer() -> (Int -> Int) {
func addOne(number: Int) -> Int {
return 1 + number
}
return addOne
}
var increment = makeIncrementer()
increment(7)
또 함수는 다른 함수를 인자로 받을 수 있습니다.
func hasAnyMatches(list: Int[], condition: Int -> Bool) -> Bool {
for item in list {
if condition(item) {
return true
}
}
return false
}
func lessThanTen(number: Int) -> Bool {
return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(numbers, lessThanTen)
실제로 함수는 클로저(Closure)의 특별한 예입니다. 중괄호로 묶어서 이름을 지정하지 않고도 클로저를 사용할 수 있습니다. in
키워드를 사용해 인자와 반환값 타입을 분리해 사용할 수도 있습니다.
numbers.map({
(number: Int) -> Int in
let result = 3 * number
return result
})
실습 모든 홀수값에 대해 0을 반환하도록 클로저를 수정해보자.
클로저를 간결하게 사용하는 몇가지 옵션이 있습니다. 델리게이트(delegate)를 위한 콜백(callback)처럼 이미 클로저의 타입을 아는 경우라면 매개변수의 타입, 반환 값 타입을 모두 생략하거나 선택적으로 생략할 수 있습니다. 한 줄짜리 구문을 가진 클로저라면 구문만 가지고도 반환 타입을 추측할 수 있습니다.
numbers.map({ number in 3 * number })
매개변수의 이름 대신에 번호로 참조하는 것은 클로저를 짧게 만드는데 특히 유용합니다. 이때 클로저는 함수의 바로 뒤에 중괄호를 이용해 인자로 전달됩니다.
sort([1, 5, 3, 12, 2]) { $0 > $1 }
클래스를 만들기 위해서는 클래스 이름과 함께 class 키워드를 사용하면 됩니다.클래스 컨텍스트(context) 내부를 제외하고 클래스 안에 속성을 선언하기 위해서는 상수나 변수를 선언하는 것과 똑같은 방식으로 쓰면 됩니다. 마찬가지로 메서드와 함수도 선언할 수 있습니다.
class Shape {
var numberOfSides = 0
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
어떤 클래스의 인스턴스를 만들려면 클래스 이름 다음에 괄호를 넣으면 됩니다. .(점) 문법을 사용하면 인스턴스의 속성이나 메서드에 접근할 수 있습니다.
var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()
현재 상태의 Shape클래스는 중요한 것이 빠져있습니다. 바로 클래스가 생성될 때 클래스를 초기화하기 위한 초기화자 입니다. init 키워드를 사용해 만들어 봅시다.
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(name: String) {
self.name = name
}
func simpleDescription() -> String {
return "A shape with \(numberOfSides) sides."
}
}
인자의 name과 속성의 name을 구분하기 위해서 self 키워드가 어떻게 사용되는지 주의해서 봅시다. 클래스의 인스턴스를 만들 때 초기화자에 인자를 전달하는 방식은 함수에 전달하는 방식과 동일합니다. 모든 속성은 numberOfSides 처럼 값을 선언 할 때 혹은 name처럼 클래스를 초기화 할 때 처럼 적어도 둘중에 한가지 방법을 통해 값을 할당해줘야 합니다.
오브젝트를 해제하기전 정리 작업이 필요하다면 deinit을 사용해서 디이니셜라이저(deinitializer)를 만들 수 있습니다.
하위 클래스는 클래스명 뒤에 상위 클래스의 이름을 세미콜론으로 구분해 포함합니다. 꼭 기본 루트 클래스가 필요한 것은 아니기 때문에 상위 클래스를 포함해도 되고 생략해도 됩니다.
하위 클래스에서 상위 클래스에서 구현된 메서드를 오버라이드(override) 하려면 override 키워드를 이용해 표시해줘야 합니다. override키워드를 사용하지 않고 어떤 메서드를 갑자기 오버라이드하면 컴파일러에서 에러로 인식합니다. 또 override 키워드를 사용했는데 실제로 상위 클래스에는 해당 메서드가 없다면 이것 또한 컴파일러가 잡아냅니다.
class Square: NamedShape {
var sideLength: Double
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 4
}
func area() -> Double {
return sideLength * sideLength
}
override func simpleDescription() -> String {
return "A square with sides of length \(sideLength)."
}
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()
실험 NamedShape클래스의 또 다른 하위 클래스인 Circle을 만들어보자. 이 클래스는 이니셜 라이저를 통해 radius와 name을 인자로 받는다. Circle 클래스 안에 area, describe 함수를 구현해보자.
저장되어 있는 간단한 속성 외에도 속성은 getter와 setter를 가질 수 있습니다.
class EquilateralTriangle: NamedShape {
var sideLength: Double = 0.0
init(sideLength: Double, name: String) {
self.sideLength = sideLength
super.init(name: name)
numberOfSides = 3
}
var perimeter: Double {
get {
return 3.0 * sideLength
}
set {
sideLength = newValue / 3.0
}
}
override func simpleDescription() -> String {
return "An equilateral triagle with sides of length \(sideLength)."
}
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
triangle.perimeter
triangle.perimeter = 9.9
triangle.sideLength
perimeter의 setter안에서는 newValue라는 이름이 새로운 값을 나타내고 있습니다. 반면에 명시적으로 set 뒤에 괄호를 이용해 명시적으로 이름을 지정해 줄수도 있습니다.
EquilateralTriangle 클래스의 이니셜라이저는 세가지의 다른 단계를 가지고 있음을 살펴봅시다.
속성의 값을 계산할 필요는 없지만 새로운 값을 할당하기 전이나 후에 수행해야할 코드가 있다면 willSet, didSet을 사용할 수 있습니다. 예를 들면 아래쪽에 나오는 클래스에서는 삼각형의 빗면의 길이가 사각형의 옆면의 길이와 항상 동일하다는 것을 보장합니다.
class TriangleAndSquare {
var triangle: EquilateralTriangle {
willSet {
square.sideLength = newValue.sideLength
}
}
var square: Square {
willSet {
triangle.sideLength = newValue.sideLength
}
}
init(size: Double, name: String) {
square = Square(sideLength: size, name: name)
triangle = EquilateralTriangle(sideLength: size, name: name)
}
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
triangleAndSquare.square.sideLength
triangleAndSquare.triangle.sideLength
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
triangleAndSquare.triangle.sideLength
클래스에 있는 메서드들은 함수와는 다른 중요한 특징이 한가지 있습니다.함수에서 사용되는 매개변수의 이름들은 함수 내부에서만 사용합니다. 그러나 메서드에 사용되는 매개변수의 이름은 메서드를 호출할 때도 사용됩니다(첫번째 매개변수의 이름은 제외하고). 기본적으로 메서드는 호출할 때 혹은 메서드에서 사용되는 이름과 동일한 매개변수 이름을 갖습니다. 하지만 메서드 내부에서 사용될 두번째 이름을 특별히 정해줄 수도 있습니다.
class Counter {
var count: Int = 0
func incrementBy(amount: Int, numberOfTimes times: Int) {
count += amount * times
}
}
var counter = Counter()
counter.incrementBy(2, numberOfTimes: 7)
옵션 값을 사용할 때는 메서드나 속성, 서브스크립트 앞에 ?를 쓸 수 있습니다. 만약 ? 앞에 값이 nil 이라면 ? 이후에 나오는 모든 것은 무시되고 표현의 값들은 nil을 갖습니다. 반면에 값이 있는 경우라면 ? 이후의 모든 그 값을 기준으로 동작합니다. 양쪽 경우 모두 옵션 값으로 사용됩니다.
enum 키워드를 사용하면 열거형을 만들 수 있습니다. 클래스나 모든 알려진 타입들의 경우 열거형에 메서드를 포함할 수 있습니다.
enum Rank: Int {
case Ace = 1
case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King
func simpleDescription() -> String {
switch self {
case .Ace:
return "ace"
case .Jack:
return "jack"
case .Queen:
return "queen"
case .King:
return "king"
default:
return String(self.toRaw())
}
}
}
let ace = Rank.Ace
let aceRawValue = ace.toRaw()
실험 두개의 Rank 값의 원본 값을 비교하는 함수를 만들어보자.
위의 예를 보면 열거형의 원본 값의 타입은 int 입니다. 그래서 특별히 첫번째 원본값만 가지고 있는 것입니다. 나머지 원본 값들은 순서에 따라 자동으로 할당됩니다. 또 문자열이나 실수형태의 값들도 원본값으로 가진 열거형을 만들 수 있습니다.
toRaw와 fromRaw 함수를 사용해서 원본값과 열거형 값을 상호 변환할 수 있습니다.
if let convertedRank = Rank.fromRaw(3) {
let threeDescription = convertedRank.simpleDescription()
}
열거형의 구성값들은 실제 값을 쓰는 다른 방법일 뿐 아니라. 모두 실제 값입니다. 사실 실제값이 의미있는 경우가 아니라면 굳이 첫번째 값을 제공할 필요가 없습니다.
enum Suit {
case Spades, Hearts, Diamonds, Clubs
func simpleDescription() -> String {
switch self {
case .Spades:
return "spades"
case .Hearts:
return "hearts"
case .Diamonds:
return "diamonds"
case .Clubs:
return "clubs"
}
}
}
let hearts = Suit.Hearts
let heartsDescription = hearts.simpleDescription()
실험 Suit에 color 메서드를 추가해 보자. color 메서드는 스페이드와 클로버는 'black'을 반환하고 하트와 다이아몬드는 'red'를 반환하도록 하면 됩니다.
열거형의 구성중 하나인 Heart가 상단값을 어떻게 참조하는지 두가지 방법을 살펴봅시다. 상수 hearts에 값을 할당 할 때, 상수는 명시적으로 타입을 가지고 있지 않기 때문에 Suit.Hearts 처럼 전체 이름을 살펴봅니다. 스위치문 안에서는 열거형은 .Hearts로 축약형을 써서 참조하는데, 이것은 self가 이미 suit라는 것을 알고 있기 때문입니다. 값의 형을 이미 알고 있다면 언제든지 축약형을 사용해도 됩니다.
구조체를 만들기 위해서는 struct 키워드를 사용합니다. 구조체는 메서드나 이니셜라이저 같은 클래스와 비슷한 기능들을 지원합니다. 클래스와 구조체의 가장 중요한 차이점중 하나는 구조체의 경우 코드 내에서 전달될 때 값복사 형태로 전달되지만 클래스의 경우에는 참조복사 형태로 전달된 다는 것입니다.
struct Card {
var rank: Rank
var suit: Suit
func simpleDescription() -> String {
return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
}
}
let threeOfSpades = Card(rank: .Three, suit: .Spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
실험 각 카드를 rank와 suit를 조합해서 만들어 전체 카드를 만들어 주는 메서드를 Card에 추가해보자.
열거형 구성자의 인스턴스는 인스턴스와 함께 관련값들을 사용할 수 있습니다. 같은 열거형의 구성자의 인스턴스들은 각 인스턴스와 함께 다른 관련값을 가질 수 있습니다. 인스턴스를 만들 때 관련값을 공급할 수 있습니다. 관련값과 원본값은 다릅니다. 열거형 구성자의 원본값은 모든 인스턴스에서 같은 값(열거형을 정의할 때 지정한 값)을 갖습니다.
예를 들면, 일출, 일몰 시간을 서버에 요청한다고 가정해보자. 서버는 두 시간 모두를 응답하거나 에러 정보를 응답할 수도 있다.
enum ServerResponse {
case Result(String, String)
case Error(String)
}
let success = ServerResponse.Result("6:00 am", "8:09 pm")
let failure = ServerResponse.Error("Out of cheese.")
switch success {
case let .Result(sunrise, sunset):
let serverResponse = "Sunrise is at \(sunrise) and sunset is at \(sunset)."
case let .Error(error):
let serverResponse = "Failure... \(error)"
}
실험 ServerResponse에 세번째 경우를 추가하고 스위치문에도 추가해보자.
스위치문의 경우의 수와 비교를 수행하기 위해 ServerResponse로 부터 일출, 일몰 시간 값을 어떻게 추출해 내는지 눈여겨 보자.
프로토콜을 선언하기 위해 protocol 키워드를 사용하자
protocol ExampleProtocol {
var simpleDescription: String { get }
mutating func adjust()
}
클래스, 열거형, 구조체 모두에 프로토콜을 사용할 수 있다.
class SimpleClass: ExampleProtocol {
var simpleDescription: String = "A very simple class."
var anotherProperty: Int = 69105
func adjust() {
simpleDescription += " Now 100% adjusted."
}
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription
struct SimpleStructure: ExampleProtocol {
var simpleDescription: String = "A simple structure"
mutating func adjust() {
simpleDescription += " (adjusted)"
}
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription
실험 프로토콜을 사용하는 열거형을 만들어보자.
구조체를 수정하기 위해 사용되는 메서드를 표시하기 위해 SimpleStructure 선언부에 사용되는 mutating 키워드를 살펴봅시다. SimpleClass에는 mutating으로 표시된 메서드가 필요하지 않습니다. 왜냐하면 클래스 안에 있는 모든 메서드들은 항상 클래스를 수정할 수 있기 때문입니다.
extension 키워드를 사용해서 기존의 타입들에 새로운 메서드나 속성들을 비교하기 위한 기능들을 추가할 수 있습니다. 타입이 선언된 곳 어디서든 혹은 라이브러리나 프레임워크에서 불러온 타입들에 extension 키워드를 사용해 프로토콜을 적용할 수 있습니다.
extension Int: ExampleProtocol {
var simpleDescription: String {
return "The number \(self)"
}
mutating func adjust() {
self += 42
}
}
7.simpleDescription
실험 extension을 사용해 Double 타입에 absoluteValue 속성을 추가해보자.
프로토콜 이름은 다른 알려진 변수들 처럼 지정할 수 있습니다. 예를 들면 객체들의 모음을 만들 때, 모든 객체는 다른 타입을 가지지만 하나의 프로토콜을 따릅니다. 프로토콜 타입인 값들을 가지고 작업할 때 프로토콜 외부에서 메서드를 정의하는 것은 불가능 합니다.
let protocolValue: ExampleProtocol = a
protocolValue.simpleDescription
// protocolValue.anotherProperty // Uncomment to see the error
protocalValue 변수가 실행시 SimpleClass 타입이더라도 컴파일러는 주어진ExampleProtocal 타입으로 취급합니다. 이것은 프로토콜 관습 외에도 클래스에서 구현된 메서드나 속성에 실수로 접근할 수는 없다는 것을 의미합니다.
제네릭 함수나 타입을 만들려면 꺾쇠안에 이름을 쓰면 됩니다.
func repeat<ItemType>(item: ItemType, times: Int) -> ItemType[] {
var result = ItemType[]()
for i in 0..times {
result += item
}
return result
}
repeat("knock", 4)
클래스, 열거형, 구조체와 마찬가지로 함수나 메서드를 제네릭 형태로 만들 수 있습니다.
// Reimplement the Swift standard library's optional type
enum OptionalValue<T> {
case None
case Some(T)
}
var possibleInteger: OptionalValue<Int> = .None
possibleInteger = .Some(100)
특정 요구 조건들의 타입 뒤에 where
키워드를 사용해 봅시다. 예를 들어 프로토콜 구현의 위한 타입을 요구하거나 똑같은 타입을 요구하는 경우 혹은 특정 상위 클래스를 요구하는 경우 말입니다.
func anyCommonElements <T, U where T: Sequence, U: Sequence, T.GeneratorType.Element: Equatable, T.GeneratorType.Element == U.GeneratorType.Element> (lhs: T, rhs: U) -> Bool {
for lhsItem in lhs {
for rhsItem in rhs {
if lhsItem == rhsItem {
return true
}
}
}
return false
}
anyCommonElements([1, 2, 3], [3])
실험 anyCommonElements 함수를 공통적으로 두개의 연속값을 갖는 배열을 반환하도록 수정해 봅시다.
간단한 경우에는 where을 생략하고 프로토콜과 클래스 이름을 콜론뒤에 바로 쓸 수 있습니다. <T:Equatable>
과 <T where T: Equatable>
은 동일합니다.
Translator : FlashMaestro (masterofflash@nate.com) Translator : Snowcat8436 (snowcat8436@gmail.com)
Swift는 iOS와 OS X 앱을 개발하기 위한 새로운 프로그래밍 언어 입니다. 그러나 C나 Objective-C로 개발하면서 얻었던 경험들과 많은 부분이 유사할 것입니다.
Swift는 정수형을 위한 Int
, 부동소숫점 값을 위한 Double
과 Float
, 불리언값을 위한 Bool
, 문자열 데이터를 위한 String
을 포함해 C와 Objective-C의 기본적인 데이터 타입에서 약간 변형된 형태로 제공합니다. 또한 컬랙션 타입
으로 통칭되는 Array
와 Dictionary
이 두가지 주요한 컬랙션 타입 또한 강력한 형태로 제공합니다.
Swift도 C처럼 식별 가능한 이름을 가지고 값을 참조하거나 저장하기 위한 변수를 사용합니다. 또한 변경 불가능한 값들 또한 폭넓게 사용되도록 했습니다. 보통 상수라고 알려져 있는데, 이것은 C에서의 상수보다 훨씬 강력합니다. 상수는 변경될 필요가 없는 값을 가지고 작업하려고 할 때 코드를 조금 더 안전하고 깔끔하게 만들 수 있도록 Swift 전반에 걸쳐 쓰이게 됩니다.
Swift는 잘 알려진 타입들 뿐 아니라 Objective-C에는 없었던 고급 타입들도 선보이고 있습니다. 값들의 묶음을 만들고 전달할 수 있도록 하는 튜플도 이 고급 타입들 중에 하나입니다. 튜플은 함수의 반환값으로 여러개의 값을 하나로 결합해 돌려줄 수 있도록 합니다.
Swift는 어떤 값의 부재를 다룰 수 있는 선택형 타입도 제공 합니다. 이 선택형은 "값이 존재하고, 그 값은 x입니다." 혹은 "값이 존재 하지 않습니다."라고 할 수 있습니다. 선택형은 Objective-C의 포인터에서 nil
을 사용하는 것과 비슷합니다. 하지만 클래스 뿐만 아니라 어떤 타입에도 사용할 수 있습니다. 선택형은 Objective-C의 nil
포인터보다 훨씬 안전하고 쓰임새 있습니다. 또 Swift의 강력한 기능들중 핵심적인 기능입니다.
선택형은 Swift가 type safe
하다는 예시 입니다. Swift는 당신이 코드를 통해 다루는 값들의 타입을 확실히 하는 것을 돕습니다. 당신이 만든 코드중에 일부가 String
타입을 사용해야 할 때 타입 세이프는 Int
같은 값을 전달하는 실수를 막아줍니다. 이를 통해 개발하는 동안 가능한한 빨리 에러는 인지하고 고치는 것이 가능합니다.
상수와 변수는 어떤 이름(maximumNumberOfLoginAttempts
나 welcomeMessage
)과 특정한 형태의 값(숫자 10
이나 문자열 Hello
)의 결합으로 구성됩니다. 상수의 값은 한번 지정되고 난 후에는 변경될 수 없고 변수는 값이 지정되고 난 이후에도 변경될 수 있습니다.
상수와 변수는 사용되기 전에 선언되어야 합니다. 상수는 let키워드, 변수는 var 키워드를 가지고 정의할 수 있습니다. 여기 사용자가 로그인을 시도한 횟수를 추적하는데 사용되는 변수와 상수를 만드는 예제가 있습니다.
let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0
이 코드는 다음과 같이 해석할 수 있습니다.
"maxiumNumberOfLoginAttempts
라는 새로운 상수를 선언하고 값은 10으로 할당한다. 그리고 currentLoginAttemp
라는 변수를 선언하고 0이라는 초기값을 할당한다."
예제에서 보면 최대값은 변하지 않기 때문에 로그인을 시도할 수 있는 최대 횟수가 상수에 정의되어 있습니다. 그리고 매번 로그인을 실패할 때마다 숫자가 증가해야 하기 때문에 현재 로그인 시도 횟수는 변수로 정의되어 있습니다.
콤마로 구분해서 한줄에 여러개의 상수나 변수를 선언하는 것도 가능합니다.
var x = 0.0, y = 0.0, z = 0.0
노트 코드를 작성할 때 변경할 필요가 없는 값을 저장하는 경우 항상
let
키워드를 사용해 상수로 선언하라. 그리고 변경할 필요가 있을 경우에만 변수로 선언하라.
상수나 변수를 만들 때 어떤 형태의 값이 저장될 지 명확하게 하기 위해 타입을 명시할 수 있습니다. 상수나 변수의 이름뒤에 콜론을 쓰고 한칸을 띄우고 사용하고 싶은 타입의 이름을 써서 타입을 명시할 수 있습니다.
다음 예시는 welcomeMessage
라는 변수에 String
값이 저장될 수 잇다는 것을 표시하기 위해 타입 명시를 하는 것입니다.
var welcomeMessage: String
콜론은 "~타입 의"라는 의미를 가집니다. 따라서 위의 코드는 다음과 같이 해석할 수 있습니다.
"String
타입의 변수 welcomeMessage
를 선언한다."
"String
타입의" 라는 말은 "어떤 String
값이든 저장할 수 있다."라는 의미 입니다. "어떤타입의"(혹은 "어떤 종류의") 라는 것은 그것이 저장될 수 있다 라는 의미로 생각하면 됩니다.
이제 welcomeMessage
변수에는 오류없이 어떤 문자열 값이든 저장할 수 있습니다.
welcomeMessage = "Hello"
노트 연습중에 타입 명시를 해야하는 경우는 드물다. 만약 상수나 변수를 정의하는 지점에 초기값을 지정한다면, Swift는 그 상수나 변수를 위해 사용할 타입을 추측한다. 이것이 바로
타입 세이프
와타입 추정
이다. 위의 예제에서welcomeMessage
에 초기값을 지정하지 않았다. 그래서 초기값으로 부터 타입을 추정하기 힘들기 때문에 타입을 명시해준 것이다.
상수와 변수의 이름을 지정하기 위해서 유니코드를 포함한 어떤 문자든지 사용할 수 있습니다.
let π = 3.14159
let 你好 = "你好世界"
let 🐶🐮 = "dogcow"
상수와 변수의 이름에는 수학기호, 화살표, 개인용(혹은 유효하지 않은) 유니코드, -선, 상자 그리기용 문자 등을 사용할 수 없다. 또 숫자로 시작해서도 안되고 이름 중간에 숫자가 들어가서도 안됩니다.
특정 타입의 상수나 변수를 한번 선언 했다면, 같은 이름으로 다시 선언하는 것이나 다른 형태의 값을을 저장하도록 하는 것은 불가능 합니다. 또 변수에 상수를 저장하거나 상수에 변수를 저장하는 것 또한 불가능 합니다.
노트 만약 Swift 예약어로 상수나 변수명을 만들고 싶다면 변수명을 ```표시로 묶어서 쓸 수 있다. 그러나 정말 다른 대안이 없는 경우가 아니면 사용하지 않는 것이 좋다.
기존 변수의 값을 호환 가능한 다른 값으로 변경할 수 있습니다. 예를 들면 friendlyWelcome
의 값은 "Hello!"
에서 "Bonjour!"
로 변경됩니다.
var friendlyWelcome = "Hello!"
friendlyWelcome = "Bonjour!"
// friendlyWelcome is now "Bonjour!
변수와는 다르게 상수는 한번 값이 정해지면 변경할 수 없습니다. 컴파일할 때 에러가 발생하도록 시도해 봅시다.
let languageName = "Swift"
languageName = "Swift++"
// this is a compile-time error - languageName cannot be changed
println
함수를 사용하면 상수와 변수의 현재 값을 출력할 수 있습니다.
println(friendlyWelcome)
// prints "Bonjour!
println
은 출력하기 적절하게 줄단위로 끊어서 값을 출력해주는 전역 함수 입니다. Xcode에서 작업을 하고 있다면 println
을 사용하면 Xcode의 "console"창에 결과가 나옵니다. (다음으로 print
함수는 println
함수와 동일한 기능을 수행하지만 값을 출력하기 위해 한줄의 끝을 표시해줄 필요가 없습니다.)
println
함수는 전달된 어떤 String
값이든 출력해줍니다.
println("This is a string")
// prints "This is a string
println
함수는 코코아의 NSLog
함수와 비슷한 방식으로 복잡한 로그 메시지를 출력하는 것도 가능합니다. 메시지에는 상수와 변수의 현재값을 포함할 수 있습니다.
Swift는 긴 문자열에서 상수나 변수명을 대체문자로 사용해 Swift가 상수나 변수의 현재 값으로 즉시 대체할 수 있도록 문자열 해석 방식을 사용합니다. 다음과 같이 이름을 괄호로 감싸고 이스케이프 시키기 위해 여는 괄호 앞에 백슬래시를 써주면 됩니다.
println("The current value of friendlyWelcome is \(friendlyWelcome)")
// prints "The current value of friendlyWelcome is Bonjour!
노트 문자열 해석에 관한 모든 옵션은
문자열 해석
부분에 설명되어 있습니다.
스스로 상기하기 위해서 혹은 메모하기 위해 코드내에 실행되지 않는 글을 쓰려고 할 때 주석을 사용할 수 있습니다. 작성한 코드를 컴파일 할 때 Swift의 컴파일러는 주석을 무시합니다.
Swift의 주석은 C의 주석과 흡사합니다. 한줄 주석은 /
(슬래시)를 두번 연속해서 쓰면 시작됩니다.
// this is a comment
또 여러줄 주석도 쓸 수 있습니다. 여러줄 주석은 슬래시와 별표를 쓰고(/*
) 끝에는 별표와 슬래시(*/
)를 순서대로 써주면 됩니다.
/* this is also a comment,
but written over multiple lines */
C의 여러줄 주석과는 다르게 Swift에서는 여러줄 주석 안에 다른 여러줄 주석을 쓸 수 있습니다. 내부 여러줄 주석을 쓰려면 첫번째 여러줄 주석 부분을 시작하고 두번째 여러줄 주석을 첫번째 주석 안에서 시작합니다. 그리고 두번째 주석을 닫아준 후 첫번째 주석을 닫아주면 됩니다.
/* this is the start of the first multiline comment
/* this is the second, nested multiline comment */
this is the end of the first multiline comment */
내부 여러줄 주석은 이미 코드에 여러줄 주석을 포함하고 있더라도 넓은 범위의 코드를 빠르고 쉽게 주석처리 할 수 있게해줍니다.
많은 다른 언어들과는 다르게 Swift는 코드의 각 문장 끝마다 세미콜론(;
)이 꼭 필요하지는 않습니다. 하지만 쓰고 싶다면 써도 됩니다. 하지만 한줄에 여러 문장을 처리하려고 한다면 세미콜론이 꼭 필요 합니다.
let cat = "🐱"; println(cat)
// prints "🐱"
정수는 42
나 -23
같이 소수점 단위가 없는 숫자들 전체 입니다. 정수는 부호가 있는 것(양수,0, 음수)와 부호가 없는 것(양수, 0) 모두를 포함합니다.
Swift는 8, 16, 32, 64 비트 형태로 부호있는 정수와 부호없는 정수를 지원합니다. 정수형은 부호 없는 8비트 정수형 UInt8
, 부호있는 32비트 정수형 Int32
처럼 C와 비슷한 관습을 따른 이름을 갖고 있습니다. Swift의 다른 모든 타입과 마찬가지고 정수형 타입명 역시 대문자로 시작합니다.
각 정수형 타입에 최대값과 최소값은 min
과 max
속성을 가지고 접근할 수 있습니다.
let minValue = UInt8.min // minValue is equal to 0, and is of type UInt8
let maxValue = UInt8.max // maxValue is equal to 255, and is of type UInt8
이런 속성값들은 예제에서 볼 수 있는 Uint8
과 같은 타입들의 적정 범위를 나타내는 값이기 때문에 다른 타입들에도 동일 표현으로 사용할 수 있습니다.
대부분의 경우 코드 내에서 사용하기 위해 특정 크기를 지정할 필요가 없습니다. Swift가 현재의 플랫폼에 해당하는 워드(word) 크기를 갖는 Int
라는 추가 타입을 지원하기 때문입니다.
Int
는 Int32
와 동일한 크기를 갖습니다.Int
는 Int64
와 동일한 크기를 갖습니다.정수형의 특정 크기가 필요한 것이 아니라면 코드 내에서 항상 Int
를 사용하면 됩니다. 이것은 코드가 일관성을 갖고 상호처리가 가능하도록 합니다. 32비트 플랫폼에서 조차 Int
는 넓은 범위의 정수를 포함하기에 충분할 만한 -2,147,483,648
~ 2,147,483,647
의 범위를 갖습니다.
Swift는 UInt
라는 부호없는 정수 타입도 지원합니다. 이것 또한 현재 플랫폼에 해당하는 워드 크기를 갖습니다.
UInt
는 UInt32
와 동일한 크기를 갖습니다.UInt
는 UInt64
와 동일한 크기를 갖습니다.노트 특별히 현재 플랫폼 해당 워드 크기의 부호없는 정수형이 필요할 때만
UInt
를 사용하라. 그런 경우가 아니라면 양수만 저장하는 경우일지라도Int
가 더 적절하다. 정수형을 위한Int
의 일관성이 코드가타입 세이프
와타입 추정
으로 묘사되는 다른 숫자 형태로의 변환또는 정수의 추정 타입일치가 필요한 경우를 피해 상호처리가 가능하도록 합니다.
부동 소수점 수란 3.14159
, 0.1
, -273.15
처럼 소수 부분을 갖는 숫자를 말합니다.
부동 소수점 타입은 Int
타입에 저장될 수 있는 것보다 훨씬 크거나 작은 숫자를 저장하거나 더 넓은 범위의 숫자를 표현할 수 있습니다. Swift는 두가지의 부동 소수점 타입을 제공합니다.
Double
은 64비트 부동 소수점 수를 표현합니다. 매우 크거나 특별히 정밀한 부동 소수점 값을 원할 경우 사용합니다.Float
은 32비트 부동 소수점 수를 표현합니다. 64비트의 정밀함이 필요하지 않은 경우 사용합시다.노트
Float
이 6자리의 소수를 표현할 수 있는 것에 비해Double
은 최소 15자리의 소수를 표현할 수 있는 정도의 정밀도를 갖습니다. 코드에서 다루는데 필요한 속성이나 값의 범위에 따라 적절히 부동 소수점 타입을 골라서 사용합니다.
Swift는 타입 세이프 언어입니다. 타입 세이프 언어들은 코드 내에서 다루는 값들의 타입이 명확하도록 만듭니다. 코드의 어떤 부분에서 String
타입이 기대된다면 실수로 Int
타입을 전달하는 것은 불가능합니다.
Swift가 타입 세이프이기 때문에 컴파일을 할 때 타입 검사를 수행하고 일치하지 않는 타입들에 대해서 에러로 표시합니다. 이를 통해 개발을 진행하면서 가능한 일찍 오류를 인지하고 고칠 수 있도록 합니다.
타입 검사는 다른 형태의 값들을 가지고 일을할 때 에러를 피할 수 있도록 해줍니다. 그러나 이것이 항상 상수나 변수를 선언할 때 타입을 명시해줘야 한다는 것을 의미하지는 않습니다. 필요로 하는 값의 타입을 명시해야 하지 않는 경우 Swift는 적절한 타입을 찾기 위해 타입 추정을 수행합니다. 타입 추정은 코드를 컴파일할 때 프로그래머가 공급한 값을 가지고 컴파일러가 자동적으로 특정 표현식의 타입을 알아내도록 합니다.
Swift는 타입 추정 때문에 C나 Objective-C에 비해 타입을 지정해줘야 하는 경우가 적습니다. 상수나 변수는 여전히 명시적으로 타입이 지정되지만 그 타입을 특정하는 많은 일들이 대신 수행됩니다.(*역자주: 상수나 변수는 타입 추정을 통해 타입을 확실하게 가지게 되기 때문에 타입을 지정해주기 위해 프로그래머가 해야할 일들이 줄었다는 것입니다.)
타입 추정은 상수나 변수를 초기값과 함께 선언할 때 특히 유용합니다. 종종 타입 추정은 상수나 변수가 선언되는 지점에서 문자 그대로의 값을 할당하는 것을 통해 이뤄집니다.(문자 그대로의 값이란 아래쪽 예시에서 볼 수 있는 42
나 3.14159
와 같은 소스코드에 직접적으로 쓰여져 있는 값을 말합니다.)
예를 들면, 타입을 명시하지 않고 새로운 상수를 선언할 때 42
라는 문자그대로의 값을 할당하면 Swift는 정수형처럼 보이는 숫자를 가지고 초기화를 했기 때문에 상수가 Int
값을 갖기를 원한다고 추정합니다.
let meaningOfLife = 42
// meaningOfLife is inferred to be of type Int
이와 비슷하게 부동 소수점 수를 위한 타입을 특정하지 않으면 Swift는 Double
형 타입을 생성하길 원하다고 추정합니다.
let pi = 3.14159
// pi is inferred to be of type Double
Swift는 부동 소수점 수를 위한 타입을 추정할 때 Float
보다는 항상 Double
을 선택합니다.
만약 한 표현식 안에 정수와 부동 소수점 수를 결합해서 사용하면 문맥으로부터 Double
타입이라고 추정될 것입니다.
let anotherPi = 3 + 0.14159
// anotherPi is also inferred to be of type Double
문자 그대로의 3
은 스스로 어떤 타입인지 명시되어 있지 않습니다. 또 덧셈 부분에 부동 소수점 수가 존재하기 때문에 Double
이 출력 지정 타입으로 추정됩니다.
정수 문자표현은 다음과 같이 쓸 수 있습니다.
0b
를 붙여서0o
를 붙여서0x
를 붙여서다음 정수 문자 표현들은 모두 십진수 17
을 나타냅니다.
let decimalInteger = 17
let binaryInteger = 0b10001 // 17 in binary notation
let octalInteger = 0o21 // 17 in octal notation
let hexadecimalInteger = 0x11 // 17 in hexadecimal notation
부동 소수점 수의 문자 표현은 10진수(접두어 없이) 혹은 16진수(접두어 0x
를 붙여서)가 될 수 있습니다. 이런 문자표현은 소수점 앞뒤로 항상 숫자(혹은 16진수 숫자)를 갖습니다. 또 이것은 10진수의 소수점을 나타내기 위한 대소문자 e
혹은 16진수의 소수점을 나타내기 위한 p
로 표현되는 지수를 가지고 있을 수도 있습니다.
exp
지수를 가지고 있는 10진수는 기수에 10의 exp승을 곱해 얻을 수 있습니다.
1.25e2
는 1.25 × 10^2, 나 125.0
을 뜻합니다.1.25e-2
는 1.25 × 10^-2, 나 0.0125
를 뜻합니다.exp
지수를 가지고 있는 16진수는 기수에 2의 exp승을 곱해 얻을 수 있습니다.
0xFp2
는 15 × 2^2, 나 60.0
을 뜻합니다.
-0xFp-2
는 15 × 2^-2, 나 3.75
를 뜻합니다.다음 부동 소수점 수의 문자표현은 모두 10진수 12.1875
의 값을 갖습니다.
let decimalDouble = 12.1875
let exponentDouble = 1.21875e1
let hexadecimalDouble = 0xC.3p0
숫자의 문자표현은 좀 더 쉽게 읽을 수 있도록 추가 형식을 포함하기도 합니다. 정수나 소수 모두 좀 더 읽기 쉽도록 여분의 0이나 _(underscores)를 포함할 수 있습니다. 두 양식 모두 문자표현의 실제 값에는 영향을 주지 않습니다.
let paddedDouble = 000123.456
let oneMillion = 1_000_000
let justOverOneMillion = 1_000_000.000_000_1
당신의 코드에서 모든 일반적인 목적으로 사용되는 정수형 상수나 변수가 모두 양수임을 알고 있더라도, 이를 위해 int
타입을 사용 할 수 있다. 하지만 위와 같은 모든 상황에서 기본적인 정수형 타입을 사용하는 것은 당신의 코드에서 정수형 상수들이나 변수들이 당신에 코드내에서 즉시 서로 정보를 교환한다는 것을 의미한다. 그리고 이는 문자 그대로의 정수형을 의미하는데 적절하다.
다른 정수형 타입들은 오직 외부의 소스로 부터오거나, 성능혹은 메모리와 같은 최적화가 필요하여 명확한 크기의 데이터를사용하는 작업이 필요한 경우에만 사용한다. 이런 상황에서 명확한 크기의 타입들을 사용하는 것은 어떠한 돌발적인 오버플로우나 사용되는 데이터의 종류를 기록하는데 도움이 된다.
정수형의 상수나 변수에 저장 가능한 숫자의 범위는 각 숫자의 타입에 따라 다르다. Int8
타입의 상수나 변수의 경우 -128
~127
의 값을 저장할 수 있으며, 반면 UInt8
타입의 상수나 변수는 0
~255
의 값을 저장할 수있다. 지정된 크기의 정수형 타입에 맞지않는 숫자는 컴파일시 에러를 출력한다:
let cannotBeNegative: UInt8 = -1
// UInt8 cannot store negative numbers, and so this will report an error
let tooBig: Int8 = Int8.max + 1
// Int8 cannot store a number larger than its maximum value,
// and so this will also report an error
각 숫자 타입은 각각 저장할 수있는 값의 범위가 다르기 때문에, 숫자의 형 변환은 경우에 따라 맞는 경우로 선택하여야 한다. 이러한 접근은 숨겨진 변환 에러들을 방지해주고 코드에서 타입을 변환하고 있다고 명시적으로 이야기하는것을 도와준다.
특별한 숫자 타입에서 다른 타입으로 변환하기 위해서, 존재하는 값을 원하는 타입의 숫자를 초기화 한다. 아래의 예시에서, 상수 twoThousand
는 UInt16
이고, 반면에 상수 one
은 UInt8
이다. 이 두값은 서로 같은 타입이 아니기 때문에 이대로 서로 더하는 것은 불가능 하다. 대신에 이 예제에서 처럼 UInt16(one)
를 이용하여 one
의 값을 가진 UInt16
타입을 새로 만들 수 있다. 그리고 이는 원래 값이 있었던 것처럼 사용할 수있다.
let twoThousand: UInt16 = 2_000
let one: UInt8 = 1
let twoThousandAndOne = twoThousand + UInt16(one)
이제는 덧셈의 양쪽 값이 UInt16
이기 때문에, 덧셈이 가능하다. 그 결과값(twoThousandAndOne
)는 두 UInt16
의 덧셈이기 때문에 Uint16
타입을 가진다.
SomeType(fInitialValue)
는 Swift타입의 기본적인 초기화 방법이자 초기화 값을 전달하는 방법입니다. 보이지 않는 곳에서, UInt16
은 UInt8
값을 변화하는 initializer를 가지고 있고, 이 initializer는 존재하는 UInt8
에서 새로운 UInt16
를 만드는데 사용된다. 하지만 어느 타입이나 넘길 수 있는 것은 아닙니다. 이는 UInt16
만을 initializer는 받아들이게 되어있습니다. 존재하는 타입을 확장하여 initializer에게 자신만이 정의한 타입을 포함해서 새 타입을 받아들이게 하고 싶다면 Extensions를 보세요.
실수와 정수사이의 변환은 분명하게 만들어야 한다:
let three = 3
let pointOneFourOneFiveNine = 0.14159
let pi = Double(three) + pointOneFourOneFiveNine
// pi equals 3.14159, and is inferred to be of type Double
위 예제에서 덧셈을 위해서 양쪽의 타입이 동일하도록 상수값 3을 Double
타입의 값으로 바꾸는 것을 볼 수있다. 만일 이러한 변환이 없다면, 덧셈은 가능하지 않을 것이다.
또한 반대로 실수를 정수로 바꾸어 계사나는 것도 가능하다, 다만 이 경우 Double
이나 Float
값을 정수로 초기화하는 과정이 필요하다.
let integerPi = Int(pi)
// integerPi equals 3, and is inferred to be of type Int
실수형 값은 위와 같은 방식으로 새로운 정수형 값으로 변환시에 항상 소숫점 이하의 값을 버림한다. 예를들면 4.75는 4가되고 -3.9는 -3이 된다.
NOTE 숫자 상수나 변수를 결합하기위한 규칙은 numeric literals의 규칙과는 다릅니다. numeric literals는 그 스스로가 명시적인 타입을 가지고 있지 않기 때문에 literal value 3은 literal value
0.14159
와 바로 더할 수 있습니다. 이는 그들의 타입은 오직 컴파일로 체크한다는 것을 의미합니다.
타입알리아스는 이미 존재하는 타입을 또다른 이름으로 정의하는것을 이야기합니다. typealias
라는 키워드로 타입 알리아스를 정의할 수 있습니다.
타입 알리아스는 외부의 소스에서온 특정한 사이즈를 가진 데이터로 작업하는 경우 처럼 이미 존재하는 타입을 보다 문맥에 맞는 이름으로 알아보고 싶을때 유용하다:
typealias AudioSample = UInt16
당신이 타입알리아스를 정의하는 즉시, 당신은 그 타입알리아스를 원래의 이름대신 사용할 수있습니다:
var maxAmplitudeFound = AudioSample.min
// maxAmplitudeFound is now 0
위에서 UInt16
을 위한 AudioSample
알리아스를 볼 수 있다. 이것이 알리아스이기 때문에 maxAmplitudeFound
를 위한 변수인 AudioSample.min
은 실제로는 UInt16.min
의 값인 0
을 의미한다.
Swift는 Bool
이라는 기본적인 이진형 타입을 가진다. 이진형 값은 논리적으로 취급되며, 그때문에 그들은 오직 참과 거짓의 두가지 값을 가진다. Swift는 이를 위해 true
와 false
라는 두가지의 상수값을 제공한다:
let orangesAreOrange = true
let turnipsAreDelicious = false
orangesAreOrange
와 turnipsAreDelicious
의 타입은 이진형의 문자 그대로의 값으로 초기화가 가능한 Bool
타입을 가지고 있습니다. 위에서의 Int
와 Double
처럼, 만일 당신이 true
나 false
로 값을 설정한다면 자동으로 형태가 결정될 것이기 때문에 Bool
로서 상수나 변수를 선언할 필요가 없다.
타입 추론(Type inference)은 상수나 Swift의 코드를 더욱 간결하고 읽기 쉽게 하는데 도움을 준다.
이진형 값은 다음과 같은 if문처럼 조건문으로 작업하는 경우에 특히 유용하다:
if turnipsAreDelicious {
println("Mmm, tasty turnips!")
} else {
println("Eww, turnips are horrible.")
}
// prints "Eww, turnips are horrible."
위의 if문과 같은 조건문에 대한 것은 Control Flow에서 조금 더 자세히 다룰것이다.
다음 예제는 컴파일 타임의 에러를 보여준다:
let i = 1
if i {
// this example will not compile, and will report an error
}
그러나 다음의 예제는 올바르게 처리되는 것을 보여준다:
let i = 1
if i == 1 {
// this example will compile successfully
}
i == 1
의 비교 결과는 Bool
타입이고, 그때문에 두번째 예제는 타입 체크를 통과할수 있다. i == 1
과 같은 비교구문은 기본연산자에서 다룰 것입니다.
다른 Swift의 타입 세이프예제와 마찬가지로, 이러한 접근은 돌발적인 에러를 피할 수 있게 해주고, 을 코드의 특정한 부분에서의 의도를 항상 분명하게 하는 것을 보장한다.
튜플은 여러 값들을 하나의 값으로 묶어준다. 튜플안의 여러 값들은 어느 타입도 가능하고, 각각 동일한 타입일 필요도 없다.
아래의 예시에서 (404, "Not Found")
는 HTTP의 상태코드를 묘사하는 튜플이다. HTTP상태 코드는 당신이 웹페이지에 요청을 할때 웹서버의 상태를 알려주는 특별한 코드이다. 요청한 페이지가 존재하지 않는다면 404 Not Found라는 상태 코드가 반환된다.
let http404Error = (404, "Not Found")
// http404Error is of type (Int, String), and equals (404, "Not Found")
(404, "Not Found") 튜플은 HTTP 상태코드를 두개의 나누어진 값으로 표현하기 위한 Int
값 하나와String
값 하나를 서로 묶은 것이다. 이것은 " (Int, String)
타입의 튜플"이라고 말할 수있다.
당신은 어떠한 순서를 가진타입들로부터도 튜블들을 만들 수 있습니다. 그리고 그 튜블들은 당신이 원하는 만큼 서로 다른 타입들을 가질 수 있습니다. 튜플 (Int, Int, Int)나 튜플(String, Bool) 혹은 당신이 필요한 어떠한 순서를 가진 튜플이라면 어떠한 것도 당신을 막을 수 없습니다. (모두 생성 가능하단의미)
당신은 튜플의 각 내용들을 분리된 상수나 변수로 분해할수 있고 이는 평상시처럼 접근도 가능합니다.
let (statusCode, statusMessage) = http404Error
println("The status code is \(statusCode)")
// prints "The status code is 404"
println("The status message is \(statusMessage)")
// prints "The status message is Not Found"
만일 당신이 튜플의 값들중 오직 몇몇개만 필요하다면, 튜플을 분리할때 튜플에서 무시할 부분을 언더바 "_" 로 처리하면 된다:
let (justTheStatusCode, _) = http404Error
println("The status code is \(justTheStatusCode)")
// prints "The status code is 404"
또다른 방식으로는 0부터 시작하는 index number를 통하여 각각의 element value에 접근합니다:
println("The status code is \(http404Error.0)")
// prints "The status code is 404"
println("The status message is \(http404Error.1)")
// prints "The status message is Not Found"
튜플을 정의할때 튜플의 각 element들에 이름을 지어줄 수도 있습니다:
let http200Status = (statusCode: 200, description: "OK")
만일 튜플의 각 element에 이름을 지어줬다면, 각 element에 값에 접근하기 위해서 element의 이름을 사용할 수 있습니다:
println("The status code is \(http200Status.statusCode)")
// prints "The status code is 200"
println("The status message is \(http200Status.description)")
// prints "The status message is OK"
튜플들은 함수들의 리턴값으로써 특히 유용합니다.
웹페이지를 검색하기위한 함수는 보통 페이지 검색을 성공여부를 표현하기 위해서 (Int, String)
튜플을 리턴합니다.두 개의 서로 다른 타입의 구분되는 값을 가진 튜플을 리턴하는 것으로 그 함수는 단순히 오직 하나의 타입을 가진 하나의 값을 리턴하는 것보다 결과에 대한 보다 유용한 정보를 제공할 수 있습니다.
보다 자세한 정보는 Functions with Multiple Return Values를 참고하기 바란다.
NOTE 튜플은 연관성있는 값들을 임시로 묶는데도 유용하다. 그들은 복잡한 자료구조를 생성하기에는 알맞지 않다. 만일 당신의 자료구조가 임시적이지 않고 계속해서 사용될 것으로 생각된다면, 튜플보다는 클래스나 자료구조로 만드는 것이 나을 것이다. 보다 많은 정보가 필요하다면 Classes and Structures를 참조하기 바란다.
옵셔널은 어떠한 값이 부재인지를 체크할때 사용한다. 옵셔널이란 다음을 이야기한다:
NOTE 옵셔널에 대한 개념은 C나 Objective-C에는 존재하지 않는다. Objective-C에서 그나마 가장 가까운 개념은 메서드에서 object값을 리턴하는 대신 올바른 오브젝트가 존재하지 않는다 라는 의미로
nil
을 리턴하는 것이다. 그러나 이는 오직 오브젝트에만 적용할 수있고 구조체, 기본적인 C언어 타입들, 열거형에 대해서는 적용할 수 없다. 이런 타입들을 위해서 Objective-C 메서들은 보통 값의 부재를 의미하는NSNotFound
와 같은 특수한 값을 리턴합니다. 이러한 접근은 메서드를 부르는 쪽에서 체크나 테스트를 위한 특수한 값을 잘 알고 있다고 가정하고 있습니다. Swift의 옵셔널은 특별한 용도의 상수가 필요 없이 어떠한 타입의 값의 부재를 바로 알아낼수 있도록 만들어줍니다.
이곳에 한가지 예제가 있다. Swift의 String
타입은 String
타입을 Int
타입으로 변환하기 위한 toInt
라는 메서드를 가집니다. 그러나 모든 문자형이 정수형으로 변환 가능한것은 아닙니다. "123"이라는 문자형은 123이라는 숫자값으로 변환이 가능하지만, "hellow, world"
라는 값은 분명한 숫자값으로 변환할 수 없습니다.
아래의 예제는 String
을 Int
로 변환하기 위해 toInt
메서드를 사용하고 있다:
let possibleNumber = "123"
let convertedNumber = possibleNumber.toInt()
// convertedNumber is inferred to be of type "Int?", or "optional Int"
Because the toInt
메서드가 실패하는 것으로 보아, 이는 int
가 아닌 optional Int값을 리턴하고있다. optional Int는 Int
가 아닌 Int?
로 쓴다. 물음표는 그 값이 optional하다는 것을 의미한다. 이는 그 값이 어떠한 Int
값을 가지거나 아예 전혀 값을 가지지 않는다는 것을 의미한다. (이는 Bool
이나 String
과 같은 다른 값은 가질 수 없다. 이는 오직 Int
값을 가지거나 아무값도 없을 뿐이다.)
어떠한 옵셔널이 값을 가지고있나 찾기위해서 if
문을 사용할 수 있다. 이 경우 만일 옵셔널이 값을 가지고 있다면 그 결과는 true
일 것이고 전혀 값을 가지지 않는다면 false
일 것이다.
옵셔널이 값을 가진다는 것을 확실히 알때, 옵셔널의 이름의 맨 마지막에 느낌표를 붙이는 것으로 그 근원 값에 접근할 수 있다. 여기서 물음표는 "내가 이 옵셔널은 확실히 값을 가지고 있고 이를 사용하라"라는 효과적인 말이다. 이것을 옵셔널 값의 강제 언랩핑이라고 한다:
if convertedNumber {
println("\(possibleNumber) has an integer value of \(convertedNumber!)")
} else {
println("\(possibleNumber) could not be converted to an integer")
}
// prints "123 has an integer value of 123"
if
문에 대한 보다 많은 것을 원한다면 Control Flow를 참조하기 바란다.
NOTE 느낌표를 사용하여 값이 존재하지 않는 옵셔널 값에 접근하려 시도하면 런타임 에러가 발생한다. 느낌표를 사용하여 강제 언랩핑을 하기 전에는 항상 옵셔널 값이
nil
이 아니라는 것을 확실히 해야 한다.
당신은 옵셔널이 값을 가지고 있는지를 찾고 만일 그렇다면 값을 임시로 상수나 변수로 사용하도록 만들기 위해 옵셔널 바인딩을 사용할 수 있습니다. 옵셔널 바인딩은 if
문이나 while
문에서 옵셔널 안에 값이 있는지 체크하고 이를 상수나 변수로 추출하는 것을 한번에 하기 위해 사용할 수 있다. if
문이나 while
문에 대해서 더욱 자세한 설명이 필요하다면 [Control Flow]페이지를 참고하시기 바랍니다.
if
문을 위해서 옵셔널 바인딩을 하는 경우 다음과 같이 쓸 수 있습니다:
if let constantName = someOptional {
statements
}
위 예시를 보면 당신은 possibleNumber
예제를 강제 언랩핑하는 대신 옵셔널 바인딩을 사용하는 방식으로 다시 쓸 수 있습니다:
if let actualNumber = possibleNumber.toInt() {
println("\(possibleNumber) has an integer value of \(actualNumber)")
} else {
println("\(possibleNumber) could not be converted to an integer")
}
// prints "123 has an integer value of 123"
이는 다음을 의미합니다:
“만일 possibleNumber.toInt
가 리턴한 옵셔널 int
값이 값을 가지고 있을 경우, 새로운 상수인 actualNumber
를 그 옵셔널이 가지는 값으로 설정한다.”
만일 변환이 성공적이라면, 상수 actualNumber
는 if
문의 첫번째 부분에서 사용하는 것이 가능하다. 이는 옵셔널이 가지는 값으로 이미 초기화 되어있고, !를 뒤에 붙여서 그 값을 가져오는 것이 필요하지 않다. 예제에서 actualNumber
는 단순히 변환의 결과를 출력하기위해 사용한다.
옵셔널 바인딩은 변수와 상수 모두에 사용할 수 있다. 만일 if
문의 첫번째 문장에서 actualNumber
의 값을 조종하는 것을 원한다면, actualNumber
를 변수로 사용할 수 있다. 그러면 옵셔널을 가지는 그 값을 상수대신 변수로서 만들 수 있다.
네가 값이 없는 상태의 옵셔널 변수를 원한다면 특별한 값인 nil
로 옵셔널 변수를 설정하면 된다:
var serverResponseCode: Int? = 404
// serverResponseCode contains an actual Int value of 404
serverResponseCode = nil
// serverResponseCode now contains no value
NOTE
nil
은 옵셔널이 아닌 상수나 변수와 사용할 수 없다. 만일 네 코드에 있는 상수나 변수가 명확한 조건하에서 값의 부재에 대응하기를 원한다면, 항상 적절한 타입의 옵셔널 값으로 그것을 선언하여야 한다.
만일 네가 값을 제공하지 않고 옵셔널 상수나 변수를 정의한다면, 그 상수나 변수는 당신을 위해 자동적으로 nil
로 설정할 것이다:
var surveyAnswer: String?
// surveyAnswer is automatically set to nil
NOTE Swift의
nil
은 Objective-C에서의nil
과 같지 않다. Objective-C에서nil
은 존재하지 않는 오브젝트를 위한 포인터라면, Swift에서nil
은 포인터가 아니고 명확한 값의 부재를 이야기할 뿐이다. 따라서 오브젝트 타입들뿐만 아니라 어떠한 타입의 옵셔널들이라도nil
로 설정하는 것이 가능하다.
위에서 이야기한 것과 같이 옵셔널들은 상수나 변수가 "값을 가지지 않는 것"을 허용한다는 것을 나타낸다. 옵셔널은 값이 있는지 없는지를 보기 위해 if
문을 이용하여 체크할 수 있고, 값이 존재하는 옵셔널의 값에 접근하기 위해 옵셔널 바인딩을 통한 조건부 언랩핑이 가능하다.
때때로 옵셔널은 처음으로 값을 설정한 이후에는 값을 가지고 있다는 것은 프로그램 구조적으로 명확하다. 이러한 경우 위 사항들은 옵셔널의 값에 접근할때마다 체크하고 언랩핑해야하는 과정을 없애는데 유용하다. 이런 이유때문에 안전하게 항상 값을 가진다고 가정할 수 있다.
이러한 종류의 옵셔널들은 Implicitly Unwrapped Optionals로 정의되었다고 할 수 있다. 당신은 옵셔널로 만들기 원하는 타입 뒤에 물음표보다 느낌표를 붙이는 것으로 Implicitly Unwrapped Optional을 만들 수 있다.
Implicitly Unwrapped Optional은 옵셔널이 첫번째로 정의되고 옵셔널들이 각 포인트에서 확실하게 존재한다고 가정한 뒤에 옵셔널의 값이 존재하는지 즉시 확인할때 유용하다. Swift에서 Implicitly Unwrapped Optional의 최우선 용도는 클래스의 초기화 과정에서 소유자가 없는 참조나 무조건적인 언랩핑된 옵셔널 속성들을 설명하는 것이다.
Implicitly Unwrapped Optional은 보이지 않는 곳에서는 일반적인 옵셔널과 같다. 그러나 접근할때마다 옵셔널 값의 언랩핑이 필요 없이 옵셔널 값이 아닌 것 처럼 사용할 수도 있다. 아래의 예시는 옵셔널 String
과 무조건적인 언랩핑 옵셔널 String
의 behavior에서의 차이점을 보여준다:
let possibleString: String? = "An optional string."
println(possibleString!) // requires an exclamation mark to access its value
// prints "An optional string."
let assumedString: String! = "An implicitly unwrapped optional string."
println(assumedString) // no exclamation mark is needed to access its value
// prints "An implicitly unwrapped optional string."
Implicitly Unwrapped Optional을 그것이 사용될때마다 자동적으로 언랩핑을 하기 위한 권한이 주어진 옵셔널으로 생각 할 수 있다. 그것을 사용할때마다 느낌표를 옵셔널의 이름뒤에 붙이는 것 보다는 네가 옵셔널을 선언할때 옵셔널의 타입 뒤에 느낌표를 붙이는 것이 낫다.
NOTE 만일 네가 명확하지 않은 값에 Implicitly Unwrapped Optional로 접근을 시도할 경우, 런타임 에러가 발생한다. 그 결과는 마치 값이 명확하지 않은 일반적인 옵셔널 뒤에 느낌표를 붙인 결과와 정확히 같다.
만일 Implicitly Unwrapped Optional이 값을 가졌는지를 체크하기 위해서는 여전히 일반적인 옵셔널 처럼 다룰 수 있다:
if assumedString {
println(assumedString)
}
// prints "An implicitly unwrapped optional string."
또한 한 문장으로 옵셔널의 값을 체크하고 언랩핑하기 위한 Implicitly Unwrapped Optionals의 옵셔널 바인딩도 사용가능하다:
if let definiteString = assumedString {
println(definiteString)
}
// prints "An implicitly unwrapped optional string."
NOTE Implicitly Unwrapped Optional은 나중에 값이
nil
이 가능성이 있는 경우에는 사용할 수 없다. 어떠한 변수가nil
값을 가지는지 체크할 필요가 있는 경우에는 항상 일반적인 옵셔널 타입을 사용한다.
옵셔널은 값이 있는지 있지 않은지 체크를 할수 있게 해주고, 값이 부재한지 우아하게 대처하는 코드를 작성합니다. 그러나 이것으로는 값이 없거나 명확한 조건을 만족하지 않은 값을 제공하는 경우에 코드를 계속 실행하게 하는 것은 불가능합니다. 이러한 상황에서 당신은 코드상에서 값이 없거나 올바르지 않은 경우를 디버그하기 위한 기회를 제공하고 종료 코드를 실행하기 위해 Assertion을 발생시킬수 있습니다.
assertion은 논리적 조건이 항상 true
인지를 런타임에 체크한다. 문자 그대로, assertion은 조건이 항상 true
인 것을 “주장한다”. 더 이상의 코드를 실행시키기 전에 필수적인 조건을 만족하는 지를 확실히 하기 위해서 assertion을 사용한다. 만일 그 조건이 true
라면, 보통 코드는 계속하여 실행된다. 그러나 만일 그 조건이 false
라면 코드는 종료되고, 너의 앱도 종료될 것이다.
만일 Xcode에서 앱을 빌드하고 실행할때와 같이 당신의 코드가 디버깅 환경에서 돌아가고 있을때 assertion이 발생한다면, 어디서 올바르지 않은 상태가 발생했는지 볼 수 있고, assertion이 발생한 시점에서의 앱의 상태를 요구할 수있다. assertion은 또한 당신에게 assert가 발생한 원인에 대한 명확한 디버그 메시지도 제공한다.
당신는 전역적인 assert함수로서 assertion을 작성할 수도 있고, assert함수에게 true
와 false
를 체크할 조건과 조건이 false
일때 출력할 메시지를 넘겨줄 수 있다:
let age = -3
assert(age >= 0, "A person's age cannot be less than zero")
// this causes the assertion to trigger, because age is not >= 0
이 예제에서 코드는 오직 age >= 0
이 true
일때(age
가 음수가 아닐때)만 실행됩니다. 만일 age
의 값이 음수라면 위와 같이 age >= 0
의 결과는 false
가 되고 assertion이 발생하며 그 앱은 종료됩니다.
Assertion 메시지는 문자어구를 사용해야하는 것은 아닙니다. assertion 메시지는 다음과 같이 원하는 경우에는 생략도 가능합니다:
assert(age >= 0)
어떠한 조건이 false
가 될수 있지만 코드 실행을 계속하기 위해서는 반드시 true
이여만 하는 곳에 assertion을 사용한다. asertion 체크를 포함하는 올바른 경우들은 다음과 같다:
nil
인데 추후의 코드 실행을 위해서 nil
이 아닌 값이 필요할때.이에 관해서는 Subscripts와 Functions을 참고하시기 바랍니다.
NOTE Assertions는 당신의 앱이 종료하는 원인이고, 당신의 코드를 부적절한 조건이라도 곤란한 상황이 발생하지 않도록 디자인 하기 위한 대체물이 없다. 그렇기는 하지만 올바르지 않은 조건이 발생할 수 있는 상황에서, assertion은 어떠한 조건을 앱의 출시 전에 개발에서 강조하고 주목시키기 위한 매우 효과적인 방법이다.
Translator : 해탈 (kimqqyun@gmail.com)
연산자는 값을 확인 변경 합치기 위해 사용하는 특수 기호나 문구입니다. 예를 들어 더하기 연산자(+
)는 (let i = 1 + 2
에서 쓰이는것 같이) 두 숫자를 더합니다.
더 복잡한 연산자에 대해 예를 들자면, (if enteredDoorCode && passedRetinaScan
에서와 같이) 논리 AND 연산자 &&
가 있고, i
의 값을 1
만큼 증가시키는 것을 축약해서 표현한 ++i
증가 연산자가 있습니다.
Swift 는 대부분의 표준 C 연산자를 지원하며 일반적인 코딩 오류를 제거하는 몇가지 기능을 향상 시켰습니다. 할당연산자 (=
)대신 항등 연산자(==
)를 사용하는 실수를 방지하기 위해 값을 반환하지 않습니다.
산술연산자(+
,-
,*
,/
,%
등)가 오버플로우를 감지하고 그들을 저장하는 유형의 허용된 값의 범위보다 크거나 작아서 발생하는 예기치 않은 결과를 방지 할 수 있습니다.
당신은 오버 플로우 연산자에 설명된대로 Swift의 오버플로우 연산자를 사용하여 오버플로 값을 선택할수 있습니다. 이것은 Overflow Operaters 에 설명되어 있습니다.
C 와 달리, Swift는 부동 소수점 숫자에 나머지 (%
) 계산을 수행 할 수 있습니다. 또한 Swift는 C언어에는 없는 (A..B
)와 (A...B
)의 2가지의 범위 연산자를 제공합니다. 이 연산자들은 값의 범위를 표현하기 위한 연산자입니다.
이 장에서는 Swift의 일반적인 연산자를 설명합니다. 고급 연산자는 고급 연산자(Advanced Operator) 장에 있습니다, 그리고 사용자 정의 연산자를 정의하고 사용자 정의 형식에 대한 표준 연산자를 구현하는 방법에 대해 설명합니다.
연산자는 단항, 이진, 그리고 삼항이 있습니다.
-a
) 단항 전위 연산자를 바로 앞에 나타내고, (예 !b
) 단항 후위 연산자는 타겟이후에 즉시 나타납니다. (예 i++
)2 + 3
)a ? b : c
) 입니다.연산자에 영향을 주는 값은 피연산자입니다. 식 1 + 2
에을 보면 +
기호는 이항 연산자이며 두가지의 피연산자 값인 1
과 2
입니다.
할당 연산자는 (a = b
) 초기화자(initializes) 또는 b
의 값을 a
에 할당하는것입니다.
let b = 10
var a = 5
a = b
// a 는 이제 10 과 같습니다.
만약 오른쪽이 같은 여러 값을 가진 튜플의 경우에 그 요소는 한번에 여러개의 상수 또는 변수로 분해 될수있습니다.
let (x, y) = (1, 2)
// x 는 1 과 같고 y 는 2 와 같다.
C 와 Objective-C의 대입 연산자와는 달리, Swift의 대입 연산자 자체가 값을 반환하지 않습니다. 다음 구문은 유효하지 않습니다.
if x = y {
// x = y가 값을 반환하지 않기 때문에 이것은 유효하지 않다,
}
위 구문이 유효하지 않은 이유는, 실수로 (==
) 대신 (=
) 연산자를 사용하는것을 방지하기 위해서입니다. if x = y
가 유효하지 않게 함으로써 Swift 코드에서 이러한 종류의 오류를 방지하는데 도움이 됩니다.
Swift 는 4가지의 산술연산자가 모든 숫자 타입을 지원합니다.
+
)-
)*
)/
)1 + 2 // 3
5 - 3 // 2
2 * 3 // 6
10.0 / 2.5 // 4.0
C 및 Objective-C의 산술 연산자와는 달리 Swift 산술 연산자는 값이 기본적으로 오버플로우하는것을 허용하지 않는다. Swift 오버플로우 연산자(a &+ b
와 같은)를 사용하여 값 오버플로 동작을 선택할 수있습니다. Overflow Operators를 참조하십시오.
또한 덧셈 연산자는 문자열을 지원합니다.
"hello, " + "world" // "hello, world" 와 같다
두 개의 Character
값이거나 하나는 Character
값 그리고 하나는 String
값일때 두 개를 함께 더해서 새로운 String
값을 만들 수 있습니다.
let dog: Character = "🐶🐶
let cow: Character = "🐮"
let dogCow = dog + cow
// dogCow is equal to "🐶🐮"
이것에 대해선 문자열과 문자(Concatenating Strings and Characters)를 참조 바랍니다
나머지 연산자는 (a % b
) b
의 몇 배수가 a
에 맞게 곱해지며 그리고 남아 있는 값을 반환합니다. (이는 나머지 라고 불립니다.)
NOTE
나머지 연산자는 (
%
) 또한 모듈로(modulo) 연산으로 다른 언어에 알려져있다. 그러나 Swift에서의 동작은 음수를 의미한다. 엄격히 말하면, 모듈로 연산보다는 나머지 연산이다.
여기에 나머지 연산의 동작이 어떻게 되는지 나와있습니다. 9 % 4
을 계산해보면, 당신은 첫번째로 9
안에 몇 개의 4
가 들어갈 수 있는지 알아낼 것이다.
당신은 4
들을 9
에 맞추었고 그리고 나머지는 1
이다. (오렌지 색깔을 보라)
Swift에서는 이렇게 쓰여집니다.
9 % 4 // 1과 같다
a % b
의 답을 측정해보면, %
연산자는 아래의 방정식을 계산하고, remainder
를 반환합니다.
a
= (b
x 배수
) + 나머지
배수
는 a
에 들어갈 b
의 최대의 숫자입니다.
9
와 4
를 식에 대입 할경우
9
= (4
× 2
) + 1
a
의 값이 음수 일때도 같은 메소드가 지원되며 나머지 값이 음수가 나옵니다.
-9 % 4 // -1과 같다
-9
와 4
를 넣으면 다음과 같은 식이 나옵니다.
-9
= (4
× -2
) + -1
나머지 값이 -1
이 주어집니다.
b
가 음수일때 부호는 무시됩니다. 이 뜻은 a % b
와 a % -b
는 항상 같은 대답을 주고 있다는 것을 의미합니다.
C 와 Objective-C의 나머지 연산과는 달리, Swift의 나머지 연산은 부동 소수점 연산 또한 지원합니다.
8 % 2.5 // 2.5와 같음
예를 들어 8
을 2.5
로 나누었을때 3
과 같으며 나머지는 0.5
와 같습니다. 그리고 나머지 연산이 반환하는 값은 Double
타입의 0.5
입니다.
C와 같이, Swift는 증가 연산자(++
)와 감소 연산자(--
)를 제공한다. 이것은 숫자 변수 1
를 증가시키거나 감소시키는 축약형입니다. 정수형과 부동소수점형을 연산자와 같이 사용 가능합니다.
var i = 0
++i // i 는 이제 1과 같다
만약 ++i
호출마다 i
의 값은 1
씩 증가됩니다. 기본적으로 ++i
는 i = i + 1
의 약어입니다. 마찬가지로 --i
를 i = i - 1
의 약어로 사용할 수 있습니다.
++
와 --
기호는 전위연산자 또는 후위연산자로 사용이 가능합니다. ++i
와 ++i
는 둘다i
의 값을1
증가시키는 방법입니다. 비슷하게, --i
와 i--
는 i
의 값을 1
감소시키는 방법입니다.
이러한 수정연산자는 i
와 그리고 반환값 까지 변화시킵니다. 만약 i
에 저장된 값을 증가 또는 감소 시킬 경우 반환값을 무시 할 수도 있습니다. 그러나 반환된 값을 사용할 경우, 당신은 다음과 같은 규칙에 따라 연산자의 전위연산자나 후위연산자를 사용하는지 여부에 따라 달라집니다.
예제 코드 (For example:)
var a = 0
let b = ++a
// a 와 b 둘다 1과 같다.
let c = a++
// a 는 지금 2 입니다. 그러나 c는 이전의 값인 1이 이미 설정되어있습니다.
위의 예제코드에서 let b = ++a
는 a
를 반환하기 전에 a
를 증가시킨다. 이 방법은 a
와 b
의 새로운 값이 동등한 이유이다.
그러나, let c = a++
는 a
를 후에 반환한 뒤 a
를 증가시킨다. 이 뜻은 c
가 없은 값은 예전의 값인 1
이며 a
에게는 업데이트 된 2
와 같습니다.
i++
의 특정동작을 필요로 하지 않는한, ++i
나 --i
를 사용하는것이 좋습니다. 왜냐하면 그것은 모든 경우에 i
를 결과를 반환하고 수정하는 예상된 동작을 가지기 때문입니다.
숫자 값의 부호는 전위연산자 -
를 사용하여 전환할 수 있다. 이것은 단항 마이너스 연산자로 알려진것입니다.
let three = 3
let minusThree = -three // minusThree equal -3
let plusThree = -minusThree // plus equal 3, or "minus minus three"
단항 마이너스 연산자는 공백없이 값 바로 앞에 추가됩니다.
단항 플러스 연산자(+
)는 간단하게 값 앞에 추가되며 값을 변경하지 않고 값을 반환합니다.
let minusSix = -6
let alsoMinusSix = +minusSix // alsoMinusSix equals -6
플러스 연산자가 있음에도 불구하고 실제로 아무것도 하지 않지만, 당신은 또한 단항 마이너스 연산자를 사용하는 경우에 양수에 대한 코드대칭에 사용할 수 있습니다.
C와 같이 Swift는 다른 작업에 할당(=
)을 결합하는 복합 할당 연산자를 제공합니다. 한 예를 들어 덧셈 할당 연산자입니다 (+=
):
var a = 1
a += 2
// a 는 3과 같다
표현식 a += 2
는 a = a + 2
의 축약형입니다. 효과적으로 한 연산자가 가산 및 할당이 동시에 결합과 작업이 됩니다.
NOTE 복합 할당 연산자는 값을 반환하지 않습니다. 당신은
let b = a += 2
이러한 코드를 작성할수 없습니다. 예를 들어 이러한 코드는 위의 증가 및 감소 연산자와는 다릅니다.
복합 할당 연산자의 전체 목록은 Expressions 에서 찾을 수 있습니다.
Swift는 C의 표준 비교연산자를 지원합니다.
a == b
)a != b
)a > b
)a < b
)a >= b
)a <= b
)NOTE Swift는 또한 두 개체 참조가 동일한 인스턴스 객체를 참조하고 있는지 여부를 테스트 하는 연산자를 지원합니다. (
===
와!==
) 자세한 내용은 Classes and Structures를 참조하십시오.
비교 연산자의 각 문장이 참인지 여부를 나타내는 Bool
값을 반환합니다 :
1 == 1 // true, because 1 is equal to 1
2 != 1 // true, because 2 is not equal to 1
2 > 1 // true, because 2 is greater than 1
1 < 2 // true, because 1 is less than 2
1 >= 1 // true, because 1 is greater than or equal to 1
2 <= 1 // false, because 2 is not less than or equal to 1
비교 연산자는 종종 if
문 같은 조건문에 사용됩니다 :
let name == "world"
if name == "world" {
println("hello, world")
} else {
println("I'm sorry \(name), but I don't recognize you")
}
// prints "hello, world", because name is indeed equal to "world”
if
에 대한 더 많은 정보는 Control Flow를 참조하기 바랍니다.
삼항 조건 연산자는 특별한 연산자와 세개의 파트로 이루어져있습니다.
식은 이러합니다. (question ? answer1 : answer2
)
이 question
을 기초로하여 참인지 거짓인지에 따라 두 식중 하나를 평가하기 위한 축약어입니다. 만약 question
이 참이면 answer1
을 계산하고 값을 반환합니다; 그렇지 않으면 answer2
를 계산하고 값을 반환합니다.
삼항 조건 연산자는 아래의 코드에 대한 단축 표현입니다.
if question {
answer1
} else {
answer2
}
이것은 테이블 행의 픽셀 높이를 계산하는 예제입니다. 행의 헤더가 있다면 컨텐츠의 높이가 50 픽셀이상이고 행의 헤더가 없다면 20픽셀 보다 큰것입니다.:
let contentHeight = 40
let hasHeader = true
let rowHeight = contentHeight + (hasHeader ? 50 : 20)
// rowHeight 는 90과 같다
위의 예제코드는 아래 코드의 속기입니다.
let contentHeight = 40
let hasHeader = true
var rowHeight = contentHeight
if hasHeader {
rowHeight = rowHeight + 50
} else {
rowHeight = rowHeight + 20
}
첫번째 예제의 삼항 조건 연산자의 사용은 rowheight
에 단 한줄의 코드를 이용하여 올바른 값으로 설정될 수 있음을 의미합니다. 이것은 두 번째 예제코드보다 간결하고 그 값이 if
문 내에서 수정될 필요가 없기 떄문에 이것은 rowheight
가 변수가 될 필요성이 없어집니다.
삼항 조건 연산자는 두 식의 어떤 결정을 고려하는것을 위해 효율적인 속기를 제공합니다. 그러나 삼항 조건 연산자는 주의해서 다뤄야 합니다. 남용하면 그 간결함은 읽기 어려운 코드로 이어질 수 있습니다. 하나의 복합 구문에 삼항 조건 연산자와 다중 인스턴스를 결합하는것을 피하십시오.
Swift는 두 개의 범위연산자를 지원하며 이 축약어는 값의 범위를 표현합니다.
폐쇄 범위 연산자(a...b
)는 a
에서 b
까지의 범위를 정의합니다. 그리고 a
와 b
의 값을 포함합니다.
폐쇄 범위 연산자는 for-in
루프와 같이 사용하고자 하는 값 범위에서 반복할때 폐쇄 범위 연산자는 유용합니다.
for index in 1...5 {
println("\(index) time 5 is \(index * 5)")
}
// 1번쨰 반복 5 is 5
// 2번쨰 반복 5 is 10
// 3번쨰 반복 5 is 15
// 4번쨰 반복 5 is 20
// 5번쨰 반복 5 is 25
for-in
루프에 대해서는 Control Flow 항목을 참조하십시오
반 폐쇄 범위 연산자 (a..b
)는 a
에서 b
로 실행되는 범위를 정의하지만 b
가 포함되어 있지 않습니다. 처음 값은 포함하고 있지만 최종값은 아니기 때문에 반폐쇄라고 합니다.
반 폐쇄 범위는 특히 0을 기반으로한 리스트 또는 배열로 작업할때 유용합니다. 그것은 리스트의 길이(포함안되는)까지 계산하는데 유용합니다.
let names = ["Anna", "Alex", "Brian", "Jack"]
let count = name.count
for i in 0.count {
println("Person \(i + 1) is called \(names[i]")
}
// Person 1 is called Anna
// Person 2 is called Alex
// Person 3 is called Brian
// Person 4 is called Jack
배열에는 4개의 항목이 포함되어있습니다. 하지만 반 폐쇄 범위기 때문에 0..count
는 단지 3까지만 카운트 합니다. (배열의 마지막 항목의 인덱스)
배열에 대해 더 참조하고 싶다면 Arrays(배열)을 참조하세요.
논리 연산자는 true
와 false
불리언 논리 값을 수정하거나 결합합니다. Swift는 C 기반 언어의 세 가지 표준 논리 연산자를 지원합니다.
!a
)a && b
)a || b
)논리 NOT 연산자(!a
)는 불리언 논리 값인 true
값을 반전시키고 false
값은 true
가 됩니다.
논리 NOT 연산자는 전위 연산자입니다. 값 앞에 연산을 공백없이 즉시 표현 할 수 있습니다. 이것은 "not a
"로 바로 읽을 수 있으며 다음의 예제에서 볼 수 있습니다.
let allowedEntry = false
if !allowedEnrty {
println("ACCESS DENIED")
}
// prints "ACCESS DENIED"
if !allowedEntry
는 "if not allowed entry" 로 읽을 수 있습니다.
즉 allowedEntry
이 false
인 경우 라인 이후의 not allowed entry
가 true
인 경우에 해당할 경우로 실행됩니다.
이 예제에서와 같이 불리언 상수와 변수 이름의 주의 깊은 선택은 이중 부정 또는 혼란한 논리구문을 피하면서 읽기 쉽고 간결한 코드를 유지하는데 도움이 될 수 있습니다.
논리 AND 연산자(a && b
)의 전체 표현식은 두 값이 모두 true
이어야 true
가 됩니다.
반대로 두 값이 false
이면 전체 표현식 또한 false
입니다. 사실 첫번째 값이 false
인 경우 두번째 값이 평가되지 않습니다. 그것을 가능할수 없기 때문에 전체표현식이 true
와 같게 됩니다. 이는 short-circuit evaluation 로 불립니다.
이 예제에서는 두 개의 Bool
값을 고려하여 만약 두 값이 true
에만 접근할 수 있습니다.
let enteredDoorCode = true
let passedRetinaScan = false
if enteredDoorCode && passedRetinaScan {
println("Welcome!")
} else {
println("ACCESS DENIED")
}
// prints "ACCESS DENIED"
논리 OR 연산자(a || b
)는 인접한 파이프 문자로 만든 중위연산자 입니다. 전체표현식이 true
가 될 때 까지 두 개의 값 중 하나만이 참이어야 하는 논리식을 만드는데 사용합니다.
위의 논리 AND 연산자처럼 논리 OR 연산자는 식을 고려할떄 short-circuit evaluation을 사용합니다. 논리 OR식의 좌측에 true
가 해당하는 경우는 전체 표현식의 결과를 변경 할수 있기 때문에 우측은 계산되지 않습니다.
아래의 예제에서 첫 번째 Bool
값(hasDoorKey
)은 false
이지만 두 번째 값(knowsOverridePassword
)는 true
이다. 하나의 값 이true
이기 떄문 전체표현식은 true
로 평가하고 접근이 허용됩니다.
let hasDoorKey = false
let knowOverridePassword = true
if hasDoorKey || knowOverridePassword {
println("Welcome!")
} else {
println("ACCESS DENIED")
}
// prints "Welcome!"
당신은 여러 논리 연산자를 결합하여 복합 논리 연산자를 만들 수 있습니다.
if enteredDoorCode && passedRetinaScan || hasDoorKey || knowOverridePassword {
println("Welcome!")
} else {
println("ACCESS DENIED")
}
// prints "Welcome!"
이 예제는 &&
및 ||
연산자를 여러개 사용하여 긴 복합 표현식을 만들었습니다. 그러나 &&
와 ||
연산자는 여전히 두 개의 값에 대해 작동하므로 이는 실제로 서로 세개가 연결된 작은 표현입니다.
만약 우리가 문의 코드를 입력하고 망막 검사를 통과한경우; 우리가 유효한 도어 키가 있는 경우이거나 긴급 재정의 암호를 알고있는 다음에 접근할 수 있습니다.
enteredDoorCode
와 passedRetinaScan
그리고 hasDoorKey
의 값에 기초하여 처음 두 개의 작은 표현식은 false
입니다. 그러나 긴급 재정의 암호가 true
로 알려져있습니다 ,그래서 전체 복합 표현식은 여전히 true
로 평가됩니다.
괄호가 엄격히 필요하지 않은경우, 읽기 복잡한 표현의 의도록 쉽게 만들수 있는 경우에 괄호가 포함되는것이 유용한 경우가 종종 있다.
위의 door access 예제 코드에서 그것의 의도를 명시적으로 확인하기 위해 복합 표현식의 첫번째 부분을 괄호를 추가하는데에 유용합니다.
if (enteredDoorCode && passedRetinaScan) || hasDoorKey || knowOverridePassword {
println("Welcome!")
} else {
println("ACCESS DENIED")
}
// prints "Welcome!"
괄호는 처음 두 값을 전체 논리에서 별도의 가능한 상태의 일부로 분명히 간주되게 만듭니다. 복합식의 출력이 변하지는 않지만 전체적인 목적이 독자에게 명확해집니다. 가독성은 항상 간결함을 선호합니다; 괄호의 사용은 당신의 의도를 확실히 파악하는데 도움이 됩니다.
Translator : 해탈 (kimqqyun@gmail.com)
문자열은 "hello, world"
또는 "albatross"
와 같은 문자의 컬렉션입니다. Swift 문자열은 String
타입으로 표시되며 이는 문자 타입의 컬렉션 값 표현입니다.
Swift String
및 Character
타입은 코드와 함께 텍스트 작업에서 유니코드호환을 완벽호환하며 빠릅니다. 문자 생성 및 조작을 위한 구문은 C 문자열과 유사한 구문을 사용하여 가볍게 읽을 수 있습니다. 문자열 연결은 두 문자열을 추가할 때 +
연산자를 추가하는 것만큼 간단하며 문자열의 가변성은 Swift의 다른 값과 상수나 변수 그리고 다른 값들의 선택으로 관리됩니다.
Swift의 String
유형은 빠르고 현대적인 구현에도 불구하고 문법이 단순합니다. 모든 문자열 인코딩이 독립적인 유니코드 문자로 구성, 다양한 유니코드 표현에 접근하기 위한 지원을 제공합니다.
문자열 삽입 과정에서 상수, 변수, 리터럴 및 긴 문자열을 삽입할 수 있습니다. 이것은 사용자 정의 문자열 값을 만들어서 보여주거나 저장을 쉽게 할 수 있습니다.
NOTE Swift의
String
타입은 Foundation의NSString
클래스에 연결됩니다. 당신은 Cocoa 또는 Cocoa Touch의 Foundation 프레임워크로 작업하는 경우NSString
의 API를 이용하여String
값 호출을 만드는 것이 가능하며 또한 이 장에서 설명한String
기능도 사용 가능합니다. 또한,NSString
의 API 인스턴스를 필요로 하는String
값도 사용 가능합니다. Foundation 과 Cocoa 에 대한 자세한 정보는 Using Swift With Cocoa and Objective-C 를 참조하십시오.
코드 내에서 미리 정의된 String
값인 리터럴등을 포함할 수 있습니다. 문자열 리터럴이란 큰따옴표로 둘러싸인 텍스트 문자의 고정된 순서입니다.
문자열 리터럴은 상수나 변수의 초기값을 제공하는것에 사용될 수 있습니다.
let someString = "Some string literal value"
Swift는 초기화된 문자열 리터럴 값으로 someString
상수에 대한 String
의 형식을 유추합니다.
문자열 리터럴은 다음과 같은 특수 문자를 포함할 수 있습니다.
\0
(null 문자), \\
(백슬래시), \t
(수평 탭), \n
(줄 바꿈), \r
(캐리지 리턴), \"
(큰따옴표), \'
(작은따옴표)\xnn
이며 nn
은 두개의 16진수 숫자입니다.\unnnn
이며 nnnn
은 4개의 16진수 숫자입니다.\Unnnnnnnn
이며 nnnnnnnn
은 8개의 16진수 숫자입니다.아래의 코드는 여러 종류의 특수문자의 예를 나타냅니다.
wiseWords
상수는 두 개의 이스케이프 문자가 포함되어 있습니다. dollarSign
과 blackHeart
및 sparklingHeart
상수는 세 가지 다른 유니코드 스칼라 문자 형식을 보여줍니다.
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
// "Imagination is more important than knowledge" - Einstein
let dollarSign = "\x24" // $, Unicode scalar U+0024
let blackHeart = "\u2665" // ♥, Unicode scalar U+2665
let sparklingHeart = "\U0001F496" // 💖, Unicode scalar U+1F496
긴 문자열을 만들기 위한 포인트를 위해 빈 String
값을 만들려면 빈 문자열 리터럴을 변수에 할당하거나 초기화 문법을 사용하여 새 String
인스턴스를 초기화합니다.
var emptyString = "" // 빈 문자열 리터럴
var anotherEmptyString = String() // 초기화 문법
// 두 문자열 모두 비어있으며 서로 똑같다.
isEmpty
의 불리언 속성을 체크하여 문자열 값이 비어있는지 여부를 확인할 수 있습니다.
if emptyString.isEmpty {
println("여기엔 아무것도 보이지 않습니다.")
}
// prints 여긴 아무것도 보이지 않습니다."
특정 String
을 변수에 할당하여(수정될 수 있는 경우) 수정(또는 변경)할 수 있는지를 나타내거나 상수(수정될 수 없는 경우)를 말합니다.
var variableString = "Horse"
variableString += " and carriage"
// variableString 은 "Horse and carriage" 입니다.
let constantString = "Highlander"
constantString += " and another Highlander"
// 컴파일 에러 - 상수 문자열은 변경될 수 없습니다.
NOTE 이 방법은 Objective-C 또는 Cocoa에서 다른 방법으로 접근합니다. 문자열이 변경될 수 있는지를 나타내기 위해 두 개의 클래스 (
NSString
또는NSMutableString
) 사이에서 선택할 수 있습니다.
Swift의 String
타입은 값 타입입니다. 새 String
값을 만드는 경우에 상수 또는 변수에 할당되면 그 문자열 값이 함수나 메소드에 전달 될 때 복사됩니다. 각각의 경우에 기존의 String
값의 새 복사본이 전달되거나 복사되며 이는 원래의 버전이 아닙니다. 값 타입은 Structurs and Enumerations Are Value Types를 참조하십시오.
NOTE 이 동작은 Cocoa에 있는
NSString
과는 다릅니다. Cocoa에 있는NSString
인스턴스를 생성할 때와 함수나 메소드에 전달하거나 변수에 할당 및 전달될 때 같은 단일NSString
에 대한 참조를 할당합니다. 특별히 요청하지 않는 한 문자열에 대해 어떠한 복사는 수행되지 않습니다.
Swift의 String
기본 복사 동작(copy-by-default)은 문자열 값이 함수나 메소드에의해 수행될 때 어디에서 오는지 상관없이 정확한 String
값을 소유하고 깨끗한지 확인합니다. 스스로 수정하지 않는 한 전달된 문자열이 수정되지 않는다는 것을 보장합니다.
내부적으로 Swift의 컴파일러는 실제 복사가 반드시 필요한 경우에만 발생하도록 최적화하고 있습니다. 이 뜻은 문자열로 작업할 때 항상 좋은 성능을 의미합니다.
Swift의 String
타입은 지정된 순서로 Character
값의 컬렉션을 나타냅니다. 각 Character
의 값은 하나의 유니코드 문자를 나타냅니다. 각 Character
에 대해 for-in
루프의 문자 반복을 사용하여 각각의 문자의 값에 접근할 수 있습니다.
for character in "Dog!🐶"{
println(character)
}
// D
// o
// g
// !
// 🐶
for-in
루프에 대해서는 For Loops 를 참조하십시오 // 링크
또한, Character
타입 표시를 제공하여 단일 문자열 리터럴에서 독립(stand-alone) Character
상수나 변수를 만들 수 있습니다.
let yenSign: Character = "¥"
문자열의 문자의 수를 검색하려면 전역 함수인 countElements
를 호출하여 함수의 유일한 매개변수인 문자열을 전달합니다.
let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪"
println("unusualMenagerie has \(countElements(unusualMenagerie)) characters")
// prints "unusualMenagerie has 40 characters"
NOTE 다른 유니코드 문자와 같은 유니코드 문자의 다른 표현은 메모리의 저장된 다른 양을 필요로 할 수 있습니다. 이 때문에 Swift의 문자는 각 문자의 표현에서 동일한 양의 메모리를 차지하지 않습니다. 결과에 따라 문자열의 길이는 차례로 그 문자의 각각 반복하지 않고는 계산될 수 없다. 당신이 특히 긴 문자열 값으로 작업하는 경우
CountElements
기능이 해당 문자열에 대한 정확한 글자수를 계산하기 위해 문자열에서 문자 세기를 반복해야 한다는 것을 인식해야 합니다. 또한countElements
에 의해 반환된 문자 수는 항상 같은 문자가 포함되어있는NSSString
의 길이 속성과 동일하지 않습니다. 길이는NSString
을 기초로 한 문자열 UTF-16 표현 내의 16bit 유닛 숫자에 기반을 두고 문자열에서 유니코드 문자의 수에 기반을 두지는 않습니다. 이 사실을 반영하기 위해 길이 속성은 Swift가NSString
문자열 값에 접근할 때utf16count
라고 합니다.
String
및 Character
를 덧셈 연산자(+
)와 함께 추가하여 새로운 문자열(또는 연결된) 값을 만들 수 있습니다.
let string1 = "hello"
let string2 = "there"
let character1: Character = "!"
let character2: Character = "?"
let stringPlusCharacter = string1 + character1 // equals "hello!"
let stringPlusString = string1 + string2 // equals "hello there"
let characterPlusString = character1 + string1 // equals "!hello"
let characterPlusCharacter = character1 + character2 // equals "!?"
또한 덧셈 할당연산자(+=)로 기존의 String
변수에 String
이나 Character
값을 추가할 수 있습니다.
var instruction = "look over"
instruction += sting2
// instriction 은 "look over there" 와 같습니다.
var welcome = "good mornig"
welcome += character1
// welcome 은 "good morning!" 과 같습니다.
NOTE
Character
값은 하나의 문자만을 포함해야만 하기 때문에 기존의Character
변수에String
이나Character
를 추가할 수 없습니다.
문자열 삽입은 상수, 변수, 리터럴 그리고 표현식을 혼합하여 이용 및 문자열 안에 문자 값을 포함하여 새로운 String
값을 만드는 방법입니다. 문자열 리터럴에 삽입된 각 항목은 백슬래시가 앞에 있으며 한 쌍의 괄호로 싸여있습니다.
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
위의 예에서 multiplier
의 값은 \(multiplier)
문자열 리터럴로 삽입됩니다. 이 플레이스홀더는 실제 문자열 삽입이 평가될 때 multiplier
의 실제 값으로 치환됩니다.
multiplier
의 값은 큰 문자열 표현식 나중의 일부입니다. 이 표현식은 Double(mutiplier) * 2.5
의 값을 계산하고 문자열로 결과 (7.5
)를 삽입됩니다. 이 경우에 문자열 리터럴 내부에 포함된 경우 표현은 \(Double(multiplier) * 2.5)
로 기록됩니다.
NOTE 문자열에 삽입된 괄호안에 쓰는 표현으로 이스케이프 큰 따옴표 (
"
) 또는 백 슬래시(\
)와 캐리지 리턴 및 줄바꿈을 포함할 수 없습니다.
Swift는 String
값을 비교하는 세가지 방법을 제공합니다 : 문자열 같음, 전위 같음, 후위 같음 // 디스커션에 올림
두개의 String
값이 동일한 순서로 포함되어 있는 경우 두개의 문자열 값이 동일한 것으로 간주됩니다.
let quotation = "We're a lot alike, you and I."
let sameQuotation = "We're a lot alike, you and I."
if quotation == sameQuotation {
pinrtln("These two strings are considered equal")
}
// prints "These two strings are considered equal"
문자열이 특정 문자열의 전위 또는 후위가 있는지를 확인하여 문자열의 hasPrefix
및 hasSuffix
메서드를 호출, String
타입의 단일 인수인 부울값을 각각 반환합니다. 두 가지 방법은 기본 문자열과 전위나 문자열 사이에 문자별 비교를 수행합니다. 두 가지 방법은 기본 문자열과 전위나 후위 및 문자열 사이의 문자별 비교를 수행합니다.
아래의 예는 셰익스피어의 로미오와 줄리엣 의 처음 두 액트인 장면의 위치를 나타내는 문자열의 배열을 고려하였습니다.
let romeoAndJuliet = [
"Act 1 Scene 1: Verona, A public place",
"Act 1 Scene 2: Capulet's mansion",
"Act 1 Scene 3: A room in Capulet's mansion",
"Act 1 Scene 4: A street outside Capulet's mansion",
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
"Act 2 Scene 1: Outside Capulet's mansion",
"Act 2 Scene 2: Capulet's orchard",
"Act 2 Scene 3: Outside Friar Lawrence's cell",
"Act 2 Scene 4: A street in Verona",
"Act 2 Scene 5: Capulet's mansion",
"Act 2 Scene 6: Friar Lawrence's cell"
]
Act 1의 장면의 수를 romeoAndJuliet
배열에 hasPrefix
를 사용하여 계산할 수 있습니다.
var act1SceneCount = 0
for scene in romeoAndJuliet {
if scene.hasPrefix("Act 1 ") {
++act1SceneCount
}
}
println("There are \(act1SceneCount) scenes in Act 1")
// prints "There are 5 scenes in Act 1"
마찬가지로 hasSiffix
메소드를 사용하여 Capulet's mansion and Friar Lawrence's cell의 장면의 수를 계산합니다.
var mansionCount = 0
var cellCount = 0
for scene in romeoAndJuliet {
if scene.hasSuffix("Capulet's mansion") {
++mansionCount
} else if scene.hasSuffix("Friar Lawrence's cell") {
++cellCount
}
}
println("\(mansionCount) mansion scenes; \(cellCount) cell scenes")
// prints "6 mansion scenes; 2 cell scenes"
uppercaseString
과 lowercaseString
속성을 가진 문자열에 대문자와 소문자 버전에 접근할 수 있습니다.
let normal = "Could you help me, please?"
let shouty = normal.uppercaseString
// shouty is equal to "COULD YOU HELP ME, PLEASE?"
let whispered = normal.lowercaseString
// whispered is equal to "could you help me, please?"
유니코드는 국제 표준 인코딩 및 텍스트를 나타내는 것입니다. 유니코드는 표준화된 형태로 거의 모든 문자를 표시하고 텍스트 파일 또는 웹페이지와 같은 외부 소스로부터 해당 문자를 읽고 쓸 수 있습니다.
Swift의 String
및 Character
유형은 유니코드를 완벽하게 준수합니다. 아래에 설명으로 그들은 서로 다른 유니코드 인코딩의 숫자를 지원합니다.
유니코드의 모든 문자는 하나 이상의 유니코드 스칼라로 표현될 수 있습니다. 유니코드 스칼라는 문자 또는 수정에 대한 고유한 21bit(그리고 이름) 입니다, 이러한 U+0061
나 LOWERCASE LATINLETTER A("a")
과 같이 U+1F425
와 FRONT-FACING BABY CHICK("🐥")
같은 경우입니다.
유니코드 문자열이 텍스트 파일이나 다른 저장소에 기록될 때 이러한 유니코드 스칼라는 여러 유니코드 정의 중 하나의 형식으로 인코딩됩니다. 각 형식은 코드 단위로 알려진 작은 덩어리의 문자열을 인코딩합니다. 이들은 UTF-8 (8bit 코드 단위로 문자열을 인코딩) 형식과 UTF-16 (16bit 코드 단위로 문자열을 인코딩) 형식을 포함하고 있습니다.
Swift는 문자열의 유니코드 표현에 접근할 수 있는 여러 가지 방법을 제공합니다.
유니코드 문자로 개별 Character
값에 접근을 for-in
구문으로 반복할 수 있습니다. 이 과정은 문자와 작업하기에 설명되어있습니다.
또한, 유니코드 호환 표현 중 하나의 String
값에 접근:
UTF-8
속성에 접근)UTF-16
속성에 접근)unicodeScalars
속성에 접근)아래의 각 예제에서는 D,O,G,! 및 (DOG FACE) 문자로 구성되어 있으며 문자열은 다른 표현을 보여줍니다. (DOG FACE
또는 `유니코드 스칼라 U+1F436)
let dogString = "Dog!🐶"
문자열의 UTF-8 속성을 반복하여 String
의 UTF-8
표현에 접근할 수 있습니다.
UTF8View
타입의 속성은 부호 없는 8 bit(UInt8
) 값의 모음이며 문자열의 UTF-8 의 각 바이트 문자열 표현입니다.:
for codeUnit in dogString.utf8 {
print("\(codeUnit) ")
}
print("\n")
// 68 111 103 33 240 159 144 182
위의 예에서 첫 번째 네개의 십진수 codeUnit
값(68
,111
,103
,33
)은 그 문자 UTF-8로 표현과 동일한 D
,o
,g
그리고 !
를 나타내며 이것들은 ASCII의 표현과 동일합니다. 마지막 네개의 codeUnit
의 값(240
,159
,144
,182
)은 DOG FACE
의 4바이트 UTF-8 표현입니다.
UTF-16 속성에 반복하여 UTF-16 표현에 접근할수 있습니다. UTF16View
타입의 속성은 부호 없는 16 bit(UInt16
)값의 모음이며 문자열의 UTF-16의 각 바이트 문자열 표현입니다.:
for codeUnit in dogString.utf16 {
print("\(codeUnit) ")
}
print("\n")
// 68 111 103 33 55357 56374
다시 처음 4가지 codeUnit
의 값(68
,111
, 103
, 33
)은 UTF-16 코드 단위의 값은 UTF-8의 문자열 표현과 같은 값을 가지며 D
,o
,g
그리고 !
의 문자를 표현합니다.
다섯 번째와 여섯 번째 codeUnit
의 값(55357
과 56374
)는 DOG FACE
문자를 UTF-16을 써로게이트 페어로 표현한것이다. 이 값은 U+D83D
(십진수 값 55357
)의 lead 써로게이트 값과 U+DC36
(십진수 값 56374
)의 trail 써로게이트 값입니다.
unicodeScalars
속성을 반복하여 String
값의 유니코드 스칼라 표현에 접근할 수 있습니다. 이 속성타입은 UnicodeScalarView
이며 UnicodeScalar
값 타입의 컬렉션입니다. 유니코드 스칼라 21bit 코드 포인트는 lead 써로게이트나 trail 써로게이트가 아닙니다.
각 UnicodeScalar
는 값 속성(value property)이 있으며 이것은 스칼라의 21bit 값을 반환합니다. UInt32
안의 값을 표현한 것입니다.:
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ")
}
print("\n")
// 68 111 103 33 128054
Value
속성들은 처음 4개의 UnicodeScalar
값(68
, 11
, 103
, 33
)을 다시 문자 D
, o
, g
와 !
를 표현합니다.
다섯 번째이면서 마지막인 UnicodeScalar
의 Value
속성은 십진법의 12804
이며 16진법 1F436
과 같습니다. 이는 DOG FACE
문자인 유니코드 스칼라 U+1F436
과 같습니다.
Value
속성들을 쿼리하는 대신 각 UnicodeScalar
값은 또한 문자열 삽입으로 새로운 String
값을 생성하는데 사용될 수 있습니다.
for scalar in dogString.unicodeScalars {
println("\(scalar) ")
}
// D
// o
// g
// !
// 🐶
Translator : 유정협 (justin.yoo@aliencube.com)
스위프트는 여러 값들을 한꺼번에 저장하기 위해 배열과 딕셔너리로 알려진 두가지 컬렉션 타입을 제공한다. 배열은 동일한 타입을 가진 값을 순서대로 저장한다. 딕셔너리는 동일한 타입을 가진 값을 순서와 상관 없이 저장한다. 따라서, 딕셔너리는 유일한 식별자인 키를 통해 값을 찾고 참조하게 된다.
스위프트에서 배열과 딕셔너리는 항상 자신이 저장하고자 하는 키와 값의 타입을 확인한다. 이것은 다른 타입을 가진 값을 배열이나 딕셔너리에 실수로라도 저장하지 못한다는 것을 의미한다. 이는 또한 배열과 딕셔너리에서 값을 가져올 때 어떤 타입의 값을 가져올 수 있는지 확신할 수 있다는 의미이기도 하다. 스위프트에서 이렇게 명시적인 타입 컬렉션을 사용하는 것은 당신의 코드가 명확한 밸류 타입을 가져야 하게끔 하는 것이며 개발시 타입이 맞는지 아닌지를 바로바로 잡아낼 수 있게끔 해준다는 것이다.
NOTE 스위프트의
Array
타입은 상수나 변수에 지정될 때, 혹은 함수나 메소드에서 사용될 때 다른 타입들과 다른 행동을 보여준다. 더 자세한 내용은 컬렉션의 변경 가능성(Mutability of Collections) 섹션과 컬렉션 타입에서 할당과 복사 형태(Assignment and Copy Behavior for Collection Types 섹션을 참고하도록 하자.
배열은 같은 타입을 가진 여러개의 값을 순서대로 저장한다. 한 배열 안에서는 같은 값이 여러 다른 위치에서 나타날 수 있다.
스위프트에서 배열은 특정한 종류들의 값들을 저장할 수 있다. 이것은 Objective-C의 NSArray
와 NSMutableArray
클라스와는 다르다. NSArray
와 NSMutableArray
클라스는 어느 종류의 객체든 저장할 수 있고, 반환하는 객체의 속성에 대한 어떠한 정보도 제공하지 않는다. 반면에 스위프트에서는 특정 배열에 저장할 수 있는 밸류 타입은 항상 명시적인 타입 선언을 통하거나 타입 추정을 통해 확인한다. 굳이 클라스 타입이 될 필요는 없다. 예를 들어 만약 당신이 Int
타입 배열을 하나 생성한다고 하면, Int
값이 아닌 어떤 값도 이 배열에 대입할 수 없다. 스위프트는 타입 지정에 대해 안전하고, 배열 안에 무슨 타입이 들어있는지를 혹은 들어갈지를 항상 확인한다.
스위프트 배열 타입을 정확하게 쓰려면 Array<SomeType>
형태로 해야 한다. 여기서 SomeType
은 배열에 저장할 타입을 의미한다. 또한 축약 형태인 SomeType[]
으로도 배열을 사용할 수 있다. 이 두 가지 형태가 기능적으로는 동일할지라도, 축약 형태를 사용하는 것을 권장한다. 이 축약 형태의 배열이 이 가이드 문서에서도 계속 쓰일 것이다.
배열은 배열 표현식을 통해서 초기화를 시킬 수 있다. 배열 표현식은 하나 또는 그 이상의 값들을 배열 컬렉션에 담는 축약 형태를 가리킨다. 배열 표현식은 대괄호로 둘러싸고, 콤마로 값들을 구분하는 형태로 하여 여러개의 값들을 표현한다.
[value1, value2, value3]
아래는 String
타입의 값들을 저장하는 shoppingList
라는 배열을 생성하는 예제이다.
var shoppingList: String[] = ["Eggs", "Mink"]
// shoppingList has been initialized with two initial items
shoppingList
변수는 "String
타입의 값들을 갖는 배열"로 정의했기 때문에 String[]
타입으로 배열 타입을 지정했다. 이렇게 String
타입을 갖는 것으로 배열 타입을 지정했기 때문에 이 배열은 오직 String
값들만을 저장할 수 있다. 여기서 shoppingList
배열은 두 "Eggs
", "Mink
" String
값을 배열 표현식으로 지정하여 초기화를 시켰다.
NOTE 이
shoppingList
배열은 다음에 나올 예제에서 더 많은 쇼핑 목록을 추가하기 때문에 상수를 위한let
introducer가 아닌var
introducer를 통해 변수로 지정했다.
이 경우에 배열 표현식은 두 String
값 이외에는 다른 것을 포함하지 않는다. 이것은 shoppingList
변수의 타입 정의 – 오직 String
타입의 값들만 저장할 수 있는 배열 – 와 일치한다. 따라서, 배열 표현식을 이용하여 shoppingList
변수를 초기화 하는 것이 허용된다.
스위프트의 타입 추정 덕분에 당신은 배열 표현식을 이용하여 같은 타입을 갖는 변수를 초기화 시킨다면 배열 타입을 쓸 필요가 없다. 따라서, shoppingList
변수의 초기화는 아래와 같이 좀 더 간결한 형태로도 가능하다.
var shoppingList = ["Eggs", "Mink"]
배열 표현식의 모든 값들이 모두 같은 타입이기 때문에 스위프트는 String[]
이 shoppingList
변수의 사용에 맞는 타입이라고 추정할 수 있다.
배열은 메소드와 프로퍼티를 통해 접근과 수정이 가능하다. 혹은 subscript 문법을 사용할 수도 있다.
배열 안에 값이 몇 개나 있는지를 확인하기 위해 읽기 전용 속성인 count
프로퍼티를 사용한다:
println("The shopping list contains \(shoppingList.count) items.")
// prints "The shopping list contains 2 items."
불리언 값을 반환하는 isEmpty
프로퍼티를 이용하면 count
프로퍼티 값이 0
인지 아닌지 곧바로 확인할 수 있다:
if shoppingList.isEmpty {
println("The shopping list is empty.")
} else {
println("The shopping list is not empty.")
}
// prints "The shopping list is not empty."
새로운 값을 배열의 마지막에 추가하는 것은 append
메소드를 이용하면 된다:
shoppingList.append("Flour")
// shoppingList now contains 3 items, and someone is making pancakes
추가 할당 연산자인 +=
를 이용하여 배열의 마지막에 새로운 값을 추가할 수도 있다.
shoppingList += "Baking Powder"
// shoppingList now contains 4 items
같은 타입을 갖는 배열 표현식을 이용하여 한꺼번에 추가시킬 수도 있다:
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
// shoppingList now contains 7 items
배열로부터 값을 찾는 것은 배열 변수 바로 뒤에 대괄호를 사용해서 찾고자 하는 값의 인덱스값을 이용하면 된다:
var firstItem = shoppingList[0]
// firstItem is equal to "Eggs"
배열의 첫번째 값이 갖는 인덱스는 0
이다. 1
이 아님을 명심하자. 스위프트에서 배열의 인덱스는 항상 0부터 시작한다.
Subscript 문법을 사용하면 지정한 인덱스에 이미 존재하는 값을 바꿀 수도 있다:
shoppingList[0] = "Six eggs"
// the first item in the list is now equal to "Six eggs" rather than "Eggs"
Subscript 문법을 이용하면 범위를 줘서 한꺼번에 값을 바꿀 수도 있다. 심지어는 바꾸려고 하는 범위가 실제 값의 크기와 달라도 그게 가능하다. 아래 예제는 shoppingList
배열에 있는 "Chocolate Spread
", "Cheese
", "Butter
" 값을 "Bananas
", "Apples
"으로 바꾸어 버린다:
shoppingList[4...6] = ["Bananas", "Apples"]
// shoppingList now contains 6 items
NOTE Subscript 문법을 사용해서 새 값을 배열의 마지막에 추가하는 것은 안된다. 만약에 배열의 크기보다 큰 인덱스 값을 사용해서 배열에 접근하려 한다면 런타임 에러를 확인할 수 있을 것이다. 하지만 유효한 인덱스 값은 사용 전에 배열의
count
프로퍼티를 이용하여 확인이 가능하다.count
프로퍼티 값이0
인 경우 – 빈 배열인 경우 – 를 제외하면 배열에서 가장 큰 인덱스 값은 항상count - 1
이 될 것이다. 인덱스는 항상0
에서 시작하기 때문이다.
특정한 인덱스에 배열 값을 넣고 싶다면 배열의 insert(atIndex:)
메소드를 이용한다:
shoppingList.insert("Maple Syrup", atIndex: 0)
// shoppingList now contains 7 items
// "Maple Syrup" is now the first item in the list
이것은 insert
메소드를 이용하여 "Mayple Syrup
"이란 새로운 값을 shoppingList
배열의 가장 앞 0
인덱스 값을 가진 곳에 넣는 것이다.
비슷한 방식으로 배열에서 값을 지울 수도 있다. removeAtIndex
메소드를 이용하면 되는데, 이 메소드는 배열내 주어진 인덱스에서 특정 값을 지우고 난 후 그 지워진 값을 반환한다. 이 지워진 값은 필요하지 않다면 무시해도 좋다.
let mapleSyrup = shoppingList.removeAtIndex(0)
// the item that was at index 0 has just been removed
// shoppingList now contains 6 items, and no Maple Syrup
// the mapleSyrup constant is now equal to the removed "Maple Syrup" string
배열에서 값을 지우고난 다음에 생기는 공백은 자동으로 지워진다. 따라서, 0
인덱스에 해당하는 값은 이제 "Six eggs
"이다:
firstItem = shoppingList[0]
// firstItem is now equal to "Six eggs"
만약 배열의 마지막 값을 지우고 싶다면 removeLast
메소드를 이용한다. 이 메소드를 이용하면 removeAtIndex
메소드를 count
프로퍼티와 함께 사용하는 불필요한 수고를 피할 수 있다. removeAtIndex
메소드와 마찬가지로 removeLast
메소드 역시 지워진 값을 반환한다:
let apples = shoppingList.removeLast()
// the last item in the array has just been removed
// shoppingList now contains 5 items, and no cheese
// the apples constant is now equal to the removed "Apples" string
for-in
반복문을 사용하면 배열 안의 모든 값들에 접근할 수 있다:
for item in shoppingList {
println(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas
만약 배열 안의 개별적인 값들과 그에 해당하는 인덱스가 함께 필요하다면 전역 함수인 enumerate
를 사용해서 배열을 돌릴 수 있다. enumerate
함수는 배열내 각각의 값에 대해 인덱스와 결합한 튜플 값을 반환한다. 반복문을 돌리는 도중 이 튜플을 변수나 상수로 분리하여 사용할 수 있다:
for (index, value) in enumerate(shoppingList) {
println("Item \(index + 1): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas
for-in
반복문에 대해서는 For 반복문 항목을 참고하도록 하자.
배열의 초기화 문법을 이용하면 초기값 할당 없이 특정 타입을 가진 빈 배열을 만들 수 있다:
var someInts = Int[]()
println("someInts is of type Int[] with \(someInts.count) items.")
// prints "someInts is of type Int[] with 0 items."
someInts
변수의 타입은 Int[]
로 추정 가능한데, 이것은 Int[]
로 초기화를 했기 때문이다.
또한 만약 컨텍스트 상에서 함수의 인자라든가 이미 타입 선언이 된 변수 혹은 상수라든가 하는 식으로 해서 이미 타입 정보를 갖고 있다면, 빈 배열을 곧바로 빈 배열 표현식을 이용하여 만들 수 있다. 빈 배열 표현식은 []
와 같이 대괄호만을 이용한다:
someInts.append(3)
// someInts now contains 1 value of type Int
someInts = []
// someInts is now an empty array, but is still of type Int[]
스위프트의 Array
타입도 특정 크기와 기본 값을 갖는 배열을 만들 수 있는 생성자를 제공한다. 배열에 들어갈 수 있는 값의 갯수(count
인자)와 기본 값(repeatedValue
인자)을 생성자에 제공하여 배열을 만들 수 있다:
var threeDoubles = Double[](count: 3, repeatedValue: 0.0)
// threeDoubles is of type Double[], and equals [0.0, 0.0, 0.0]
생성자를 사용할 때 기본 값에서 타입을 추정하기 때문에 배열 생성시 굳이 타입 지정을 할 필요가 없다:
var anotherThreeDoubles = Array(count: 3, repeatedValue: 2.5)
// anotherThreeDoubles is inferred as Double[], and equals [2.5, 2.5, 2.5]
마지막으로 이미 존재하는 같은 타입의 두 배열을 +
연산자를 통해 합치는 것만으로 새로운 배열을 생성할 수도 있다. 이렇게 만들어진 새로운 배열의 타입은 합치기 전 두 배열의 타입으로부터 추정 가능하다:
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as Double[], and equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
딕셔너리는 같은 타입을 가진 여러개의 값을 저장하는 하나의 컨테이너이다. 각각의 값은 유일한 키 값에 물려 있으며, 이 키 값은 딕셔너리 안에서 해당 값을 찾기 위한 식별자의 역할을 한다. 배열의 값들과 달리 딕셔너리 안에 저장된 값은 어떤 순서가 정해져 있지 않다. 실제로 사전에서 어떤 단어의 정의를 찾는 것과 매우 같은 방식으로 딕셔너리 안에 정의된 식별자를 이용해서 값을 찾는다.
스위프트의 딕셔너리는 특정한 타입의 키와 그에 따른 값을 저장한다. 이는 Objective-C에서 제공하는 NSDictionary
와 NSMutableDictionary
클라스와는 다르다. NSDictionary
와 NSMutableDictionary
클라스는 어느 종류의 객체든 키와 값으로 저장이 가능한 반면 그 저장된 객체의 속성에 대한 어떠한 정보도 제공하지 않는다. 스위프트에서는 특정 딕셔너리에 저장할 수 있는 키 타입과 밸류 타입은 항상 명시적인 타입 선언을 하거나 타입 추정을 통해 확인한다.
스위프트의 딕셔너리 타입은 Dictionary<KeyType, VaueType>
형태로 쓰인다. 여기서 KeyType
은 딕셔너리의 키 값으로 쓰이는 값에 대한 타입이고, ValueType
은 딕셔너리의 키 값에 맞추어 저장하고자 하는 밸류의 타입을 정의하는 것이다.
딕셔너리가 갖고 있는 유일한 제약사항은 반드시 KeyType
은 해시 가능한 타입이어야 한다. 즉, 그 자체로 유일하게 표현이 가능한 방법을 제공해야 한다는 것이다. 스위프트의 모든 기본 타입들 (String
, Int
, Double
, Bool
)은 기본적으로 해시 가능한 것들이므로 딕셔너리의 키 타입으로 사용 가능하다. 연관된 값이 없는 열거형의 멤버 값들 역시도 기본적으로 해시 가능한 타입이다. (Enumerations 참조)
딕셔너리는 딕셔너리 표현식을 통해서 초기화를 시킬 수 있다. 딕셔너리 표현식은 앞에서 살펴봤던 배열 표현식과 비슷한 문법을 갖는다. 딕셔너리 표현식은 하나 또는 그 이상의 키/밸류 쌍을 딕셔너리 컬렉션에 담는 축약 형태를 가리킨다.
키/밸류 쌍은 키와 밸류의 조합이다. 딕셔너리 표현식에서 각각의 키/밸류 쌍 안에서 키와 밸류는 콜론으로 나뉜다. 키/밸류 쌍은 리스트로써, 콤마로 나뉘고 대괄호로 감싼다:
[ key 1 : value 1 , key 2 : value 2 , key 3 : value 3 ]
아래 예제는 국제공항들의 이름들을 저장하는 딕셔너리를 생성한다. 이 딕셔너리에서 키 값은 국제공항 코드 (IATA 코드)를 나타내는 세글자 코드이며 밸류는 공항의 이름이다:
var airports: Dictionary<String, String> = ["TYO": "Tokyo", "DUB": "Dublin"]
airports
딕셔너리는 Dictionary<String, String>
타입을 갖게끔 정의했으며 이것은 "Dictionary
타입으로서 String
타입의 키, String
타입의 밸류를 갖는다"는 것을 의미한다.
NOTE
airports
딕셔너리는let
introducer를 이용한 상수형 대신var
introducer를 이용하여 변수로 정의하였다. 이는 아래 예제들에서 이 딕셔너리에 계속해서 공항들을 추가할 것이기 때문이다.
airports
딕셔너리는 두 개의 키/밸류 쌍을 포함하는 딕셔너리 표현식을 통해 초기화를 시켰다. 첫번째 쌍은 "TYO
" 라는 키에 "Tokyo
" 라는 밸류를 갖는다. 두번째 쌍은 "DUB
" 라는 키에 "Dublin
" 이라는 밸류를 갖는다.
이 딕셔너리 표현식은 두개의 String:String
쌍을 포함한다. 이것은 airports
타입의 정의인 String
타입의 키와 String
타입의 밸류를 갖는 딕셔너리와 일치한다. 따라서 딕셔너리 표현식을 이용해서 airpots
딕셔너리 변수를 두개의 초기값으로 초기화 시킬 수 있다.
배열과 같이 딕셔너리 표현식의 키/밸류 쌍이 갖는 타입이 일정하다면 딕셔너리 타입을 정의할 필요가 없다. aiports
의 초기화는 아래와 같은 축약 형태로 표현할 수 있다:
var airports = ["TYO": "Tokyo", "DUB": "Dublin"]
딕셔너리 표현식 안의 모든 키 값의 타입이 서로 같고, 마찬가지로 모든 밸류 타입이 서로 같기 때문에, 스위프트는 Dictionary<String, String>
타입이 airports
딕셔너리에 적용 가능하다고 추정할 수 있다.
딕셔너리는 메소드와 프로퍼티를 통해 접근과 수정이 가능하다. 혹은 subscript 문법을 사용할 수도 있다. 배열과 같이 딕셔너리 안에 값이 몇 개나 있는지를 확인하기 위해 읽기 전용 속성인 count
프로퍼티를 사용한다:
println("The dictionary of airports contains \(airports.count) items.")
// prints "The dictionary of airports contains 2 items."
딕셔너리에 새 아이템을 추가하기 위해 subscript 문법을 사용할 수 있다. 같은 타입의 새 키를 subscript 인덱스로 사용하여 같은 타입의 새로운 밸류를 할당한다:
airports["LHR"] = "London"
// the airports dictionary now contains 3 items
Subscript 문법을 사용하여 특정 키에 물려 있는 값을 변경시킬 수도 있다:
airports["LHR"] = "London Heathrow"
// the value for "LHR" has been changed to "London Heathrow"
또다른 subscripting 방법으로써, 딕셔너리의 updateValue(forKey:)
메소드를 사용하여 특정 키에 해당하는 값을 설정하거나 변경할 수 있다. 위의 Subscript 예제와 같이 updateValue(forKey:)
메소드는 만약 키가 존재하지 않을 경우에는 값을 새로 설정하거나 키가 이미 존재한다면 기존의 값을 수정한다. 하지만 subscript와는 달리 updateValue(forKey:)
메소드는 업데이트를 하고난 뒤 이전 값을 반환한다. 이렇게 함으로써 실제로 업데이트가 일어났는지 아닌지를 확인할 수 있게 된다.
updateValue(forKey:)
메소드는 딕셔너리의 밸류 타입에 해당하는 Optional
값을 반환한다. 예를 들어 어떤 딕셔너리가 String
밸류를 저장한다면 이 메소드는 String?
타입 또는 "Optional String
" 타입의 밸류를 반환한다. 이 Optional 밸류는 만약 키가 이미 있었다면 수정하기 이전 밸류를, 아니라면 nil
을 갖는다:
if let oldValue = airports.updateValue("Dublin International", forKey: "DUB") {
println("The old value for DUB was \(oldValue).")
}
// prints "The old value for DUB was Dublin."
Subscript 문법을 이용하면 특정 키 값에 대응하는 밸류를 딕셔너리에서 찾을 수 있다. 값이 존재하지 않는 키를 요청할 수 있기 때문에 딕셔너리는 딕셔너리의 밸류 타입에 해당하는 Optional 밸류를 반환한다. 만약 딕셔너리가 요청한 키에 대응하는 밸류를 갖고 있다면, Subscript 는 그 키에 대응하는 밸류를 Optional 밸류를 반환한다. 아니라면 Subscript는 nil
을 반환한다:
if let airportName = airports["DUB"] {
println("The name of the airport is \(airportName).")
} else {
println("That airport is not in the airports dictionary.")
}
// prints "The name of the airport is Dublin International."
Subscript 문법을 이용해 nil
값을 특정 키에 할당하는 것으로 딕셔너리에서 키/밸류 쌍을 삭제할 수 있다:
airports["APL"] = "Apple International"
// "Apple International" is not the real airport for APL, so delete it
airports["APL"] = nil
// APL has now been removed from the dictionary
또는 키/밸류 쌍을 딕셔너리에서 삭제할 때 removeValueForKey
메소드를 이용할 수 있다. 이 메소드는 키/밸류 쌍을 삭제하고 삭제된 값을 반환하거나 값이 없다면 nil
을 반환한다:
if let removedValue = airports.removeValueForKey("DUB") {
println("The removed airport's name is \(removedValue).")
} else {
println("The airports dictionary does not contain a value for DUB.")
}
// prints "The removed airport's name is Dublin International."
for-in
반복문을 사용하면 딕셔너리 안의 모든 키/밸류 쌍에 접근할 수 있다. 딕셔너리 각각의 아이템은 (key, value)
튜플을 반환하고, 반복문을 돌리는 도중 이 튜플의 멤버들을 분리하여 임시 상수 혹은 변수에 할당하여 사용할 수 있다:
for (airportCode, airportName) in airports {
println("\(airportCode): \(airportName)")
}
// TYO: Tokyo
// LHR: London Heathrow
for-in
반복문에 대한 자세한 내용은 "For 반복문"*링크필요* 섹션을 참고하도록 하자.
또한 딕셔너리의 keys
, values
프로퍼티를 이용하면 키 또는 밸류 컬렉션을 반복문으로 돌릴 수 있다:
for airportCode in airports.keys {
println("Airport code: \(airportCode)")
}
// Airport code: TYO
// Airport code: LHR
for airportName in airports.values {
println("Airport name: \(airportName)")
}
// Airport name: Tokyo
// Airport name: London Heathrow
만약 딕셔너리의 키 콜렉션, 밸류 콜렉션을 Array
인스턴스를 이용하고 싶다면, 딕셔너리의 keys
, values
프로퍼티를 배열로 초기화하여 사용할 수 있다:
let airportCodes = Array(airports.keys)
// airportCodes is ["TYO", "LHR"]
let airportNames = Array(airports.values)
// airportNames is ["Tokyo", "London Heathrow"]
NOTE 스위프트의
Dictionary
타입은 순서를 정하지 않는 컬렉션이다. 키, 밸류, 키/밸류 쌍의 순서는 반복문을 돌릴때 정해지지 않는다.
배열과 마찬가지로 초기화 문법을 이용하여 비어있는 딕셔너리 타입을 만들 수 있다:
var namesOfIntegers = Dictionary<Int, String>()
// namesOfIntegers is an empty Dictionary<Int, String>
이 예제는 Int, String
타입을 갖는 빈 딕셔너리를 만든다. 키는 Int
타입, 밸류는 String
타입이다.
만약 컨텍스트에서 이미 해당 타입에 대한 정보를 제공한다면 빈 딕셔너리 표현식을 이용하여 딕셔너리를 초기화하여 만들 수 있다. 빈 딕셔너리 표현식은 [:]
으로 나타낼 수 있다:
namesOfIntegers[16] = "sixteen"
// namesOfIntegers now contains 1 key-value pair
namesOfIntegers = [:]
// namesOfIntegers is once again an empty dictionary of type Int, String
NOTE 스위프트의 배열과 딕셔너리 타입은 제너릭 컬렉션을 구현한다. 제너릭 타입과 컬렉션에 대한 더 자세한 내용은 제너릭 섹션을 참고하도록 하자.
배열과 딕셔너리는 하나의 컬렉션 안에 여러개의 값을 저장한다. 만약 어떤 변수를 배열이나 딕셔너리 형태로 만든다면 이 컬렉션은 변경이 가능하다. 이는 컬렉션이 초기화된 후에도 여기에 아이템을 더 추가한다거나 뺀다거나 하는 식으로 컬렉션의 크기를 변경시킬 수 있다는 것을 의미한다. 반면에 배열이나 딕셔너리를 상수에 할당한다면 이때에는 컬렉션의 값도, 크기도 바꿀 수 없다.
이러한 불변성 딕셔너리는 기존의 키에 대응하는 값을 바꿀 수 없다는 것을 의미한다. 다시 말해서 불변성 딕셔너리라면 한 번 값이 설정된 후에는 절대로 바꿀 수 없다.
그러나 배열에서 이러한 불변성은 살짝 다른 의미를 갖는다. 불변성 배열의 크기를 바꿀 가능성이 있는 어떤 것도 할 수 없지만, 기존의 배열 인덱스에 새로운 값을 설정하는 것은 가능하다. 이것은 배열의 크기가 고정될 경우, 스위프트의 Array
타입에 배열 연산과 관련하여 최적의 성능을 제공한다.
스위프트가 제공하는 Array
타입의 변경 가능성은 또한 어떻게 배열 인스턴스가 생성되고 변경되는지에 대해서도 영향을 미친다. 더 자세한 내용은 컬렉션 타입에서 할당과 복사 형태 섹션을 참조하도록 하자.
NOTE 컬렉션의 크기를 변경시킬 필요가 없는 경우에는 불변성 컬렉션을 만드는 것이 좋다. 이렇게 함으로써 스위프트 컴파일러가 컬렉션의 퍼포먼스에 최적화를 시킬 수 있다.
Translator : 김나솔(nasol.kim@gmail.com)
Swift 언어에서는 C언어 같은 프로그래밍 언어에서 제공하는 것과 비슷한 제어문 관련 구조를 제공합니다. 이러한 구조에는 for
나 while
이 있으며, 이러한 키워드는 어떤 작업(task)을 여러 번 수행합니다. if
와 switch
문은 특정 조건이 충족되는지에 따라서 분기시켜서 코드 블럭을 실행시킵니다. 또한 break
나 continue
같은 구문은 실행의 흐름을 코드 상의 다른 곳으로 이동시킵니다.
C언어에서는 for
-조건부-증가(increment) 순환문(loop) 이런 방식을 전통적으로 사용하는데, Swfit에서는 for-in
순환문(loop)이라는 것이 있어서 배열이나 사전(dictionaries), ranges, 문자열(strings)등 sequence에 대해서iteration하기가 쉽습니다.
C언어의 switch
문과 비교했을 때, Swift의 switch
문은 훨씬 더 강력합니다. Swift에서는 switch
문이 "fall through" 하지 않습니다(역자주: fall through란, switch문에서 한 case에 대해서 처리하고 난 후 다음 case로 넘어가는 것). C언어에서는 실수로 break
문을 써주지 않아서 에러가 생기는 경우가 있는데 Swift에서는 fall through 하지 않기 때문에 이런 에러를 방지할 수 있습니다. swich
내의 case에 대해서 여러 종류의 pattern-maching을 사용할 수 있습니다. 수의 범위 match, 투플 match, casts to a specific type. switch case
에서 match된 값을 임시 상수나 변수에 binding할 수도 있습니다. 이렇게 binding해두면 case의 본문(body) 내에서 이 상수나 변수를 사용할 수 있습니다. 또한 매칭 조건(matching condition)이 복잡한 경우에는, 각 case에 대해서 where절(where clause)을 사용해서 표현할 수 있습니다.
for
순환문(for loop) 사용하면 코드 블럭을 특정 횟수 만큼 수행할 수 있습니다. Swift에는 두 종류의 for 순환문이 있습니다:
for-in
: 어떤 범위나 sequence, collection, progression에 대해서, 이 안에 있는 각 항목(item)에 대해서 코드(a set of statement)를 실행합니다.
for-condition-increment
: 특정 조건부가 참이 될 때까지 코드를 실행합니다. 보통 루프를 한 번 도는 것이 끝날 때마다 counter를 1씩 증가시킵니다.
여러 항목이 들어 있는 컬렉션(collection)이나, 어떤 범위, 배열 안에 들어 있는 항목(item)에 대해서, 또는 문자열에 들어 있는 각 문자에 대해서 iteration을 할 때 for-in
loop를 사용합니다.
다음의 예는 구구단의 5단에서 처음 몇 개를 출력해 줍니다:
for index in 1...5 {
println("\(index) times 5 is \(index * 5)")
}
// 1 times 5 is 5
// 2 times 5 is 10
// 3 times 5 is 15
// 4 times 5 is 20
// 5 times 5 is 25
위 예에서는 범위가 1부터 5까지의 폐쇄 영역이며, 범위에 대해서 컬렉션(collection)안에 들어 있는 각 항목(item)에 대해 이터레이션을 돌고 있습니다. 단, 1...5라고 표현했을 때, ...
(closed range operator)를 보면 알 수 있듯이, 이 범위에는 1과 5가 포함됩니다. index
의 값은 범위 내의 첫번째 수, 즉 1이 되며, 루프 내에 있는 구문이 실행됩니다. 위 예에서 루프 안에는 구문이 하나만 있습니다. index
의 현재 값에 대해서 5단의 첫번째를 출력해주는 것입니다. 이 구문이 실행된 다음에 인덱스의 값은 범위 내의 두번째 값, 즉 2가 되도록 업데이트 됩니다. 그리고 println
함수가 다시 호출됩니다. 이 작업은 인덱스가 범위의 끝에 이를 때까지 계속됩니다.
위 예에서 index
는 상수(constant)이며, 이 상수의 값은 루프를 돌 때마다, 초반에 자동으로 값이 지정됩니다. 따라서 이 상수를 사용하기 전에 선언할 필요가 없습니다. 루프를 선언할 때 포함시키기만 해도 암묵적으로 선언한 셈이 됩니다. 즉 let
선언 키워드(declaration keyword)를 써줄 필요가 없습니다.
NOTE
index
상수는 루프의 스코프(scope)안에서만 존재합니다. 루프문이 끝난 다음에 이index
의 값을 확인하고 싶거나, 이 값을 상수가 아니라 변수로 사용하고 싶으면, 루프 안에서 변수로 직접 선언해 주어야 합니다.
범위 내에 있는 값이 필요 없다면, 변수명 대신에 언더바(_
, underscore)를 써주면 됩니다:
let base = 3
let power = 10
var answer = 1
for _ in 1...power {
answer *= base
}
println("\(base) to the power of \(power) is \(answer)")
// prints "3 to the power of 10 is 59049"
위 예는 어떤 수의 몇 승을 계산해 줍니다(이 예에서는 3
의 10
승을 계산했습니다). 시작하는 값은 1
인데(이는 3
의 0
승입니다), 이 시작하는 값에 다시 3을 곱해줍니다. 이 때 0에서 시작해서 9에서 끝나는 half-closed loop를 사용하였습니다. 이 계산을 수행할 때, 루프를 도는 동안 counter의 값은 필요 없습니다. 정확한 횟수만큼 루프를 도는 것만이 중요합니다. 밑줄(_
, underscore)은 위 예에서 루프 변수 자리에 쓰였는데요, 그 결과 루프를 돌 때의 counter 현재값에 접근할 수 없게 됩니다.
리뷰어 주: 10이 포함되는 것을 보면 반개구간이 아니라 폐쇄구간인 1~10인데, 말이 잘못쓰여있는듯. 이건 원문도 그렇네요.
배열 안에 들어 있는 항목(item)에 대해서 이터레이션(iteration)을 할 때에, for-in
루프를 사용하세요.
let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
println("Hello, \(name)!")
}
// Hello, Anna!
// Hello, Alex !
// Hello, Brian!
// Hello, Jack!
딕셔너리(dictionary)에 대해서도 이터레이션(iteration)을 해서 키-값 쌍(key-value pairs)에 접근할 수 있습니다. 딕셔너리에 대해서 이터레이션을 하면, 딕셔너리의 각 항목이 (key, value)
투플의 형태로 반환됩니다. 그리고 이 (key, value)
쌍은 쪼개어져서 두 개의 상수의 값으로 들어갑니다. 이 값은 for-in
루프 내의 본문(body)내에서 사용할 수 있습니다. 아래의 예에서 딕셔너리의 키는 animalName
이라는 상수에, 딕셔너리의 값은 legCount
라는 상수에 값으로 들어갑니다:
let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
println("\(animalName)s have \(legCount) legs")
}
// spiders have 8 legs
// ants have 6 legs
// cats have 4 legs
Dictionary
안에 있는 항목이 이터레이션 될 때, 딕셔너리 안에 들어 있는 순서대로 되지는 않습니다. Doctionary
안에 들어 있는 데이터는 원래 정의상 순서가 없으며, 이터레이션을 돌 때에도, 어느 항목에 대해서 돌지 확신할 수 없습니다. 배열과 딕셔너리에 대해서 더 자세히 보시려면 컬렉션 형(Collection Types) 장을 참조하세요.
이 이터레이션 되는 순서는 고정되어 있지 않습니다. 딕셔너리 안에 들어 있는 순서대로 배열과 딕셔너리 외에도, 문자열 내의 각 문자에 대해 이터레이션을 돌 때, for-in
루프를 사용할 수 있습니다:
for character in "Hello" {
println(character)
}
// H
// e
// l
// l
// o
Swift는 for-in
루프 말고도 C언어에서 쓰는, 조건부와 증가부가 들어 있는 for
루프를 지원합니다:
for var index = 0; index < 3; ++index {
println("index is \(index)")
}
// index is 0
// index is 1
// index is 2
다음은 이번에 다루는 루프 형식을 일반화해서 나타낸 것입니다.
for initialization ; condition ; increment {
statements
}
루프의 정의부는 세 부분으로 구성되는데, C언어에서처럼 각 부분을 세미콜론;
으로 구분하고 있습니다. C언어와는 다르게 Swift에서는 "초기화;조건부;증가부" 부분을 괄호로 감싸주지 않아도 됩니다.
다음은 루프가 실행되는 단계를 나타냅니다:
false
이면, 루프는 종료하고, 코드 실행(code execution)은 for
루프를 닫는 중괄호(})다음 부분에서 계속됩니다. 조건부가 true
이면, 코드 실행은 루프를 여는 괄호({) 안에서 구문들을 실행합니다. 위에서 설명한 루프문의 형식(format)과 코드 실행 절차를 개요로 나타내면 다음과 같습니다:
initialization
while condition {
statements
increment
}
초기화 부분에서 선언된 상수와 변수(예를 들면 var index=0)는 for
루프 스코프 내에서만 사용할 수 있습니다. 루프가 끝난 후에도 index의 마지막 값에 접근할 수 있으려면, 루프 스코프가 시작하는 지점 이전에 index를 선언해주어야 합니다 (즉 루프 스코프의 바깥에서 선언해 주어야 합니다):
var index : Int // <= 이 부분에서 선언해 주어야..
for index = 0; index < 3; ++index {
println("index is \(index)")
}
// index is 0
// index is 1
// index is 2
println("The loop statements were executed \(index) times")
// prints "The loop statements were executed 3 times"
위 예에서 한 가지 주의할 점이 있습니다. 루프가 끝났을 때 index
의 마지막 값은 2
가 아니라 3
입니다. 마지막으로 증가부(increment)인 ++index
가 실행되었을 때, index
의 값은 3
이 됩니다. index
가 3
이 되니, index < 3
조건부 false
가 되서 루프가 끝난 것입니다.
while
루프는 조건부가 거짓이 될 때까지 코드 블럭을 실행시킵니다. 이런 종류의 루프는 보통, 이터레이션이 시작하기 전에 이터레이션이 몇 번이나 돌 지 알지 못할 때 자주 사용합니다. Swift는 두 종류의 while
루프를 지원합니다. 하나는 while
인데, 이 루프는 루프를 돌기 시작할 때 조건부를 검토합니다. 다른 하나는 do-while
인데, 이 루프는 루프를 돌고 나서 조건부를 검토합니다.
while
루프는 한 개의 조건부를 검토하는 것에서 시작합니다. 조건부가 true
면, 코드가 실행되며, 조건부가 false
가 될 때까지 반복해서 실행됩니다.
다음은 while
루프를 일반화해서 나타낸 것입니다:
while condition {
statements
}
이번에 사용할 예제는 뱀과 사다리 게임입니다.
게임의 규칙은 다음과 같습니다:
게임판은 Int
값들로 나타냅니다. 게임판의 크기는 finalSquare
상수로 정합니다. finalSquare는 배열을 초기화할 때 사용하며, 나중에 게임에 이겼는지 여부를 판별할 때도 사용합니다. 배열 board는 26개의 Int
값 0으로 초기화됩니다. 25개가 아닙니다. (0
부터 25
까지, 26개입니다.)
let finalSquare = 25
var board = Int[](count : finalSquare + 1, repeatedValue: 0)
몇몇 칸에는 0이 아니라 특정한 값이 지정됩니다. 이 값은 뱀과 사다리 때문에 필요한 값입니다. 사다리의 밑부분이 들어 있는 칸은 게임판에서 앞으로 이동시키는 만큼의 양수(positive number)를 포함하고, 뱀 머리가 들어 있는 칸은 게임판에서 뒤로 이동시키는 만큼의 음수(negative number)를 포함합니다:
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
3번 칸은 사다리의 밑 부분을 포함합니다. 여기에 가면 참가자는 11번 칸으로 이동합니다. 이것을 나타내기 위해서 board[03]
에 +08
값을 지정합니다. 이는 정수값 8
과 동일합니다(11
과 3
간의 차이). 단항 연산자인 plus operator(+i
)는 단항 연산자 minus operator(-i
)와 균형을 이룹니다. 또한 10
보다 작은 수에는 10자리에 0을 넣어서(예: 8 -> 08) 줄이 맞추어져 가지런하게 보이도록 했습니다. (이처럼 0을 넣는 등 스타일에 변화를 주는 것은 꼭 필요한 것은 아닙니다만, 이렇게 하면 코드가 더 깔끔하게 보입니다.)
게임 참가자가 시작하는 칸은 "0번 칸"이며, 이 칸은 게임판의 좌측 하단 바깥에 있습니다. 주사위를 처음 던지면, 참가자는 항상 게임판 위로 이동하게 됩니다:
var square = 0
var diceRoll = 0
while square < finalSquare {
// 주사위를 던진다
if ++diceRoll == 7 { diceRoll = 1 }
// 주사위를 던져 나온 수 만큼 이동한다
square += diceRoll
if square < board.count {
// 아직 게임판에 있다면, 뱀을 타고 내려가거나 사다리를 타고 올라간다
//if we're still on the board, move up or down for a snake or a ladder
square += board[square]
("Game over!")
이 예제에서는 주사위를 던지는 부분을 간단하게 처리했습니다. 난수를 발생시키지 않고, diceRoll
의 값을 0에서 시작하게 하고, 루프를 돌 때마다, diceRoll 값이 1씩 증가하도록 했습니다. ++i
(prefix increment operator)를 사용해서 말이죠. 그런 다음에 diceRoll
의 값이 너무 크지 않은지 확인했습니다. ++diceRoll
값은 diceRoll
이 1만큼 증가한 값과 같습니다. diceRoll
값이 7
과 같아지면, 값이 너무 커진 것이며, 이 때 diceRoll
값을 1로 해줍니다. 이렇게 하면 diceRoll
의 값은 항상 1
,2
,3
,4
,5
,6
,1
,2
, 등등의 값을 가지게 됩니다.
주사위를 던진 후에, 게임 참가자는 diceRoll
값 만큼 칸을 이동합니다. 주사위에서 나온 수만큼 이동했는데, 참가자가 25번 칸을 넘어가는 경우가 생길 수 있습니다. 이 경우에는 게임이 끝납니다. 이 시나리오를 따르기 위해서, 코드는 현재의 square
값에다 board[squre]
에 저장된 값을 더해서 참가자를 이동시키기 전에, square
가 board
배열의 count
값보다 작은지 확인합니다.
이렇게 확인작업을 해주지 않으면, board[squre]
라고 썼을 때 board
배열의 범위를 넘어서는 값을 접근하려고 시도하게 되고, 에러가 날 것입니다. 예를 들어 square
가 26
과 같아지면, 코드는 board[26]
의 값을 확인하려 할 것이고, 26은 배열의 수보다 큽니다.
이 while
루프 실행은 끝납니다. 그리고 루프의 조건부를 확인하고, 루프가 다시 실행되어야 하는지 확인합니다. 게임참가자가 25번 칸이나 25를 넘어서는 칸으로 이동했다면, 루프의 조건부는 거짓이 될 것이고 게임은 끝납니다.
위 예제의 경우에는 while
루프를 사용하는 것이 적절합니다. 왜냐하면 while
루프가 시작되기 전에 게임가 얼마나 계속되어야 하는지 알지 못하기 때문입니다. while
루프를 쓰면 특정 조건이 충족될 때까지 계속해서 실행됩니다.
while
루프와 비슷하지만 약간 다른 루프도 있습니다. 이름은 do-while
루프이며, 이 루프에서는 루프의 조건부가 검토되기 전에 루프 블록이 한 번 실행됩니다. 그런 다음 조건부가 거짓이 될 때까지 루프를 반복합니다.
다음은 do-while
문을 간단하게 일반화하여 나타낸 것입니다:
do {
statements
} while condition
이번에도 뱀과 사다리 게임 예제를 사용하겠습니다. 다만 이번에는 while
루프 가 아니라 do-while
루프를 사용합니다. finalSquare
, board
, square
, diceRoll
변수의 값은 앞에서 while
루프를 사용했을 때와 동일한 값으로 초기화했습니다.
let finalSquare = 25
var board = Int[](count : finalSquare + 1, repeatedValue: 0)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
이번 예제에서, 루프 내에서 처음으로 하는 작업은 사다리나 뱀이 들어 있는 지 확인하는 것입니다. 사다리를 타고 올라갔을 때 게임 참가자가 바로 25번 칸으로 이동하는 것은 불가능합니다. 즉 사다리를 타는 방법으로는 게임에서 이길 수 없습니다. 따라서 루프 안에서 칸에 뱀이나 사다리가 있는 지 여부를 확인하면 안전합니다.
게임을 시작할 때, 참가자는 “0번 칸”에 있습니다. board[0]
의 값은 항상 0이며, 이 값이 가지는 효과는 없습니다:
do {
// move up or down for a snake or ladder square += board[square]
// 주사위를 던진다
if ++diceRoll == 7 { diceRoll = 1 }
// 주사위를 던져서 나온 수만큼 이동한다
square += diceRoll
} while square < finalSquare
println("Game over!")
칸에 뱀이나 사다리가 있는지 프로그램이 확인한 후에, 주사위가 던져집니다. 게임 참가자는 diceRoll
값 만큼 칸을 움직입니다. 그런 다음 이번 루프 실행은 끝납니다.
루프의 조건부의 내용(while square < finalSquare
)은 이전 예제에서와 같지만, 이번에는 루프를 한 번 돈 다음에 이 조건부가 검토됩니다. 이 게임에는 while
루프보다 do-while
루프의 구조가 더 적절합니다. 이번에 사용한 do-while
루프에서, square += board[quare]
이 부분은 루프의 while
조건부에서 square
가 아직 board
상에 있다고 확인한 후에 항상 즉시 실행됩니다. 이렇게 하면 이전 예제에서 한 것처럼 배열의 범위를 확인할 필요가 없어집니다.
특정 조건이 충족하는지에 따라 각각 다른 코드를 실행하는 것이 유용한 경우가 많습니다. 어떤 에러가 발생했을 때 특정 코드를 실행시키고 싶을 수도 있습니다. 또는 어떤 값이 너무 높거나 너무 낮을 때 메시지를 보여주고 싶을 수도 있습니다. 이렇게 하려면 코드를 조건문으로 쓰면 됩니다.
Swift에서 코드에 조건문을 추가하는 방법은 두 가지가 있습니다. 바로 if
문과 switch
문입니다. 보통 조건의 수가 많지 않을 때에는 보통 if
문을 사용합니다. 한편 조건의 종류가 다양하고 복잡할 때에는 switch
문이 더욱 적합하니다. 실행시킬 코드 브랜치를 선택하는데 패턴-매칭이 도움이 되는 경우에도 switch
문을 사용합니다.
if
문을 아주 단순하게 표현하면, 하나의 if
조건이 있습니다. 그럼 그 조건이 참인 경우에만 코드구문들이 실행됩니다.
var temperatureInFahrenheit = 30 if temperatureInFahrenheit <= 32 {
println("It's very cold. Consider wearing a scarf.")
}
// prints "It's very cold. Consider wearing a scarf."
위 예제에서는 온도가 화씨 32도(물이 어는 온도)보다 낮은지 같은지 여부를 확인합니다. 온도가 화씨 32도 이하이면, 메시지가 출력됩니다. 그 외의 경우에는 아무런 메시지도 나오지 않으며 코드 실행은 if
문을 닫는 중괄호(}) 다음으로 이동하여 계속됩니다.
if
문을 사용할 때에는 else
절도 사용할 수 있는데, 여기에는 if
조건이 거짓일 때 실행되는 코드가 들어갑니다. else
키워드를 사용하여 나타냅니다:
temperatureInFahrenheit = 40
if temperatureInFahrenheit <= 32 {
println("It's very cold. Consider wearing a scarf.")
} else {
println("It's not that cold. Wear a t-shirt.")
}
// prints "It's not that cold. Wear a t-shirt."
위에서 두 개의 브랜치 중에서 하나는 항상 실행됩니다. 온도가 화씨 40
도로 올라갔기 때문에, 스카프를 매라고 조언할 정도로 춥지 않습니다. 따라서 else
브랜치가 대신에 실행됩니다.
추가적인 경우를 고려할 때에는 여러 개의 if
문을 쓸 수도 있습니다:
temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
println("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
println("It's really warm. Don't forget to wear sunscreen.")
} else {
println("It's not that cold. Wear a t-shirt.")
}
// prints "It's really warm. Don't forget to wear sunscreen."
위 예에서, 추가적인 if
문이 들어갔습니다. 이 부분은 온도가 특별히 높을 때의 경우에 대한 것입니다.
마지막에 쓴 else
절이 있습니다. 이 부분은 온도가 너무 높지도 낮지도 않은 경우에 보여주는 메시지를 출력합니다.
하지만 마지막에 쓴 else
절은 선택적입니다. 조건들이 모든 경우를 다뤄야 하지 않는다면, else
절을 안 써줘도 됩니다:
temperatureInFahrenheit = 72
if temperatureInFahrenheit <= 32 {
println("It's very cold. Consider wearing a scarf.")
} else if temperatureInFahrenheit >= 86 {
println("It's really warm. Don't forget to wear sunscreen.")
}
이번 예제에서 온도는 너무 낮지도 너무 높지도 않았습니다. 그래서 if
나 else if
조건부에 들어 있는 코드는 실행되지 않았습니다. 그리고 아무 메시지도 출력되지 않았습니다:
switch
문은 값을 검토해서 몇 가지 패턴과 비교합니다. 그런 다음, 처음 매칭되는 패턴이 있는 코드 블록을 실행시킵니다. if
문을 사용할 때보다 switch
문을 사용했을 때의 장점은, 여러 가지 경우에 대해서 처리할 수 있게 된다는 점입니다.
switch
문에서는 어떤 값과 한 개 이상의 동일한 타입(type)의 값을 비교합니다:
switch some value to consider {
//switch 고려하려는 값
case value 1 :
respond to value 1
case value 2 ,
value 3 :
respond to value 2 or 3
default :
otherwise, do something else
}
switch
문은 여러 가지 가능한 경우(case)로 구성되어 있습니다. 각 경우는 case
라는 키워드로 시작됩니다. 특정 값과 비교할 수도 있지만, Swift에서는 더욱 복잡한 패턴과 비교하는(complex pattern matching) 여러 가지 방법이 있습니다. 이에 대해서는 이번 섹션의 뒷부분에 나옵니다.
경우(switch case)의 본문 각각은 서로 독립된 코드실행 브랜치입니다(separate branch of code execution). 이는 if
문과 비슷합니다. switch
문은 어느 브랜치를 선택할지 결정합니다. 이는 비교하려는 값에 대해서 스위치한다고 합니다.
switch
문 안에는 가능한 경우(case)가 모두 고려되어야 합니다. 즉, 비교하려는 값이 가질 수 있는 모든 값이 해당하는 경우(case)가 반드시 있어야 합니다. 명시적으로 다루지 않는 값에 대해서는 디폴트 경우를 정의해서 처리할 수도 있습니다. 이렇게 할 때에는 키워드 default
를 사용하며, 이 경우는 switch
문 안에서 제일 마지막에 위치해야 합니다.
다음의 예에서는 switch
문을 사용해서 someCharacter 변수에 들어 있는 하나의 소문자를 검토합니다:
let someCharacter: Character = "e"
switch someCharacter {
case "a", "e", "i", "o", "u":
println("\(someCharacter) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
println("\(someCharacter) is a consonant")
default :
println("\(someCharacter) is not a vowel or a consonant")
}
// prints "e is a vowel "
위 예에서 switch
문 안의 첫번째 경우(case)는 영어의 다섯 개 모음과 매치됩니다. 마찬가지로, 두번째 경우는 영어의 자음 소문자와 매치됩니다.
자음과 모음 이외의 나머지 문자와 매치하는 경우(case)를 만들기 위해, 이 문자들을 다 써주려면 번거롭니다. 그래서 위 예제에서는 디폴트 경우(default case)를 사용하여, 자음과 모음 이외의 문자가 매치되는 경우를 처리했습니다. 이렇게 디폴트 경우를 써줌으로써 이 switch문은 빼먹은 경우(case)가 없는, 포괄적인 switch문이 됩니다.
C언어나 Objective-C의 switch
문과는 다르게, Swift에서는 디폴트로 다음 경우로 넘어가지 않습니다. 대신에, 한 경우와 매치되면, 그 경우에 해당하는 코드 블록이 실행된 후에, 전체 switch
문이 종료됩니다. 이 때 명시적으로 break
문을 써주지 않아도 됩니다. 이렇게 함으로써 C언어에서 보다 실수를 할 위험이 줄어듭니다. C언어에서는 실수로 break
문을 빠뜨리면 의도하지 않게, 한 개 이상의 경우에 해당하는 코드블럭을 실행시킬 수도 있습니다.
NOTE 필요하면, 매치된 경우 내의 코드블럭을 다 실행시키기 전에 빠져나올 수도 있습니다. 이에 대해서는 Break in a Switch Statement 부분을 참고하세요.
각 경우의 본문은 적어도 한 개 이상의 실행가능한 구문을 포함해야 합니다. 다음의 예제코드처럼 쓰면 안됩니다. 왜냐하면 첫번째 경우의 본문이 비어 있기 때문입니다:
let anotherCharacter: Character = "a"
switch anotherCharacter {
case "a":
case "A":
println("T he letter A")
default :
println("Not the letter A")
}
// 컴파일 에러가 납니다.
C언어의 switch
문에서는 anotherCharacter가 “a”
와 “A”
경우 둘 다하고 매치되는 반면에, 위 예제의 switch
문은 그렇지 않습니다. 위 예제에서는 컴파일 에러가 납니다. 왜냐하면 “a”
경우의 본문에 실행가능한 코드가 없기 때문입니다. 이런 방식 덕분에, 의도하지 않았는데 다음 경우로 넘어가는 실수를 방지할 수 있으며 의도가 더욱 명확하게 드러납니다.
한 경우에 해당하는 매치를 여러 개 쓸 때에는 콤마,
로 구분합니다. 그리고 길어지면 줄을 바꾸어 써도 됩니다:
switch some value to consider {
case value 1 ,
value 2 :
statements
}
NOTE 특정 경우의 본문이 실행된 후에, 다음 경우로 자동으로 넘어가게 하려면, 키워드
fallthrough
를 사용하면 됩니다. 자세한 사용방법은 [Fallthrough]를 참고하세요.
switch
경우 안에 있는 값에 대해서 어떤 범위 안에 들어있는지 여부를 확인할 수 있습니다. 아래의 예제는 수 범위를 사용하여, 수의 크기에 관계 없이, 어떤 수를 대략적으로 문자로 표현한 결과를 제공합니다:
let count = 3_000_000_000_000
let countedThings = "stars in the Milky Way"
var naturalCount : String
switch count {
case 0:
naturalCount = "no"
case 1...3:
naturalCount = "a few"
case 4...9:
naturalCount = "several"
case 10...99:
naturalCount = "tens of"
case 100...999:
naturalCount = "hundreds of"
case 1000...999_999:
naturalCount = "thousands of"
default:
naturalCount = "millions and millions of"
println("There are \(naturalCount) \(countedThings).")
// prints "There are millions and millions of stars in the Milky Way ."
하나의 switch
문 안에서 여러 개의 값을 사용할 때에는 튜플을 사용하면 됩니다. 튜플의 각 요소는 특정 값이나 값의 범위와 비교할 수 있습니다. 특정 값을 지정하지 않고 임의의 값을 나타내려면 밑줄 _
을 사용하세요.
아래의 예제에서는 (x, y)로 표현된 점을 취합니다. 이 점은 (Int, Int)
타입의 튜플로 나타내며, 이 점이 그래프 상에서 어느 구역에 위치하는지 분류합니다.
let somePoint = (1, 1)
switch somePoint {
case (0, 0):
println("(0, 0) 은 원점에 있습니다")
case (_, 0):
println("(\(somePoint.0), 0)은 x축 상에 있습니다. ")
case (0, _):
println("(0, \(somePoint.1))은 y축 상에 있습니다.")
case (-2...2, -2...2):
println("(\(somePoint.0), \(somePoint.1))은 상자 안에 있습니다.")
default :
println("(\(somePoint.0), \(somePoint.1))은 상자 밖에 있습니다.")
}
//prints "(1, 1) is inside the box"
switch
문은 점이 원점(0,0)에 있는지, 빨간선으로 표현된 x축 상에 있는지, 주황색으로 표현된 y축 상에 있는지, 그래프의 중앙에 파란색으로 칠한 4x4 상자 안에 있는지, 그 상자의 밖에 있는지를 판단합니다.
C언어와는 다르게, Swift에서는 하나의 값이나 값 묶음을 여러 개의 경우(case)와 비교할 수 있습니다. 사실 위 예제에서 점(0, 0)은 4개의 경우에 모두 해당됩니다. 하지만 여러 경우와 매치하는 경우에는, 첫번째 경우만 사용됩니다. 따라서 점(0,0)은 case (0,0)
하고만 매치하며, 다른 경우 안에 있는 코드는 실행되지 않습니다.
switch
문에서 경우는 매치된 값이나 값들을 임시 상수나 변수에 바인딩할 수 있습니다. 이렇게 바인딩한 상수나 변수는 그 경우의 본문에서 사용할 수 있습니다. 이렇게 하는 것을 보고 값을 묶는다, 바인딩(value binding)한다고 합니다. 왜냐하면 해당 경우의 본문 내에서 값이 특정 상수나 변수에 “묶여” 있기 때문입니다.
아래의 예제는 (x, y)
점을 취합니다. 이 점은 (Int, Int) 튜플로 표현되며, 예제는 이 점을 그래프상의 어느 구역에 위치하는지 분류합니다:
let anotherPoint = (2, 0)
switch anotherPoint {
case (let x, 0):
println("x축 상에 있으며 x의 값은 \(x)값입니다.")
case (0, let y):
println("y축 상에 있으며 y의 값은 \(y)입니다.")
case let (x, y):
println("(\(x), \(y))에 있습니다.")
}
// 다음을 출력합니다: "x축 상에 있으며 x의 값은 2입니다"
switch
문은 이 점이 빨간색선인 x축 상에 있는지, 주황색선인 y축 상에 있는지, 또는 그 이외의 구역에 있는지를 판단합니다.
위 예제에서 세 개의 switch
case에서는 플레이스홀더인 상수 x와 상수 y를 선언하였습니다. 이 둘은 anotherPoint
로부터 튜플 값을 취합니다. 첫번째 case에서 case (let x, 0)
은 y
값이 0
이면 모두 매치합니다. 이 때 점의 x
값은 임시 상수인 x
에 들어갑니다. 마찬가지로, 두번째 case인 case(0, let y)
는 x
값이 0
이면 모두 매치합니다. 그리고 이 때 점의 y값은 임시 상수인 y에 들어갑니다.
일단 임시 상수가 선언되면, case의 코드 블록 내에서 사용될 수 있습니다. 위 예제에서는 println
함수에서 값을 출력할 때 점의 좌표값을 빠르게 표현하기 위해서 이 상수를 사용하였습니다.
위 예제에는 디폴트 경우(default case)가 없다는 점에 유의하세요. 마지막 경우인 case let (x, y)
에서는 두 개의 플레이스 홀더 상수로 이루어진 튜플을 선언하는데, 이는 모든 점과 매치합니다. 결국, 남아있는 모든 가능한 값은 이 경우와 매치한다는 의미이며, 스위치 문에서 가능한 모든 경우를 다 포괄하고 있으므로 이 때에는 디폴트 경우를 쓸 필요가 없습니다.
위 예제에서는 x
와 y
의 값이 경우의 본문에서 바뀔 필요가 없습니다. 그래서 키워드 let
을 사용해서 상수로 선언하였습니다. 값이 바뀔 필요가 있다면 키워드 var
를 사용해서 변수로 선언하면 됩니다. 이렇게 하면 임시 변수가 만들어지고, 적절한 값으로 초기화됩니다. 이 변수 값의 변화는, 해당 경우 내의 본문 내에서만 영향을 미칩니다.
Where
switch
의 case에는 where
절을 사용해서 조건부를 작성할 수도 있습니다.
아래의 예제는 어떤 점 (x, y)과 그래프의 어느 구역에 위치하는지를 분류합니다:
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y :
println("(\(x), \(y)) 는 x==y인 곳에 있습니다.")
case let (x, y) where x == -y :
println("(\(x), \(y)) 는 x==-y인 곳에 있습니다.")
case let (x, y):
println("(\(x), \(y)) 는 기타 구역에 있습니다.")
}
// prints "(1, -1) 은 x==-y인 곳에 있습니다."
위 예제의 switch
문은 이 점이 x == y
인 녹색사선 상에 있는지, x==-y
인 보라색 사선 상에 있는지, 그 이외의 지점에 있는지 판단합니다.
위 예제에서 세 개의 case는 플레이스홀더 상수 x와 상수 y를 선언하였으며, 이 둘은 점의 좌표값을 받아서 튜플로 가지고 있습니다. 이 상수는 where
절에서 사용되어, 동적인 필터를 만들었습니다. 어떤 경우의 where
절 내의 조건이 참이 되어야 어떤 값이 그 경우와 매치합니다.
이전 예제에서와 마찬가지로, 위 예제의 마지막에 나오는 경우는 나머지 모든 가능한 값과 매치합니다. 따라서 디폴트 경우는 쓸 필요가 없습니다.
Control Transfer Statements 는 control을 특정 코드로부터 다른 코드로 이동시키는 방법으로 코드가 실행되는 순서를 바꿉니다. Swift에는 네 가지 흐름제어 이동문이 있습니다:
control
, break
, fallthrough
문에 대해서는 여기서 자세히 다루고, return
문은 함수(Functions) 부분에서 다룹니다.
continue
문은 루프에게 현재 하고 있는 것을 멈추고, 루프의 다음번 이터레이션을 시작하라고 명령합니다. 즉 루프를 빠져나가지 않고 “루프의 현재 이터레이션에 대해서는 끝났다”다고 말하는 셈입니다.
NOTE
for-조건부-증가부
루프에서continue
문을 호출한 이후에도, 증가자(incrementer)의 참 거짓 여부를 검토합니다. 루프 자체는 계속됩니다. 다만 루프 내 본문에 있는 코드가 건너뛰어질 뿐입니다.
다음의 예제는 소문자로 된 문자열에서 모든 모음과 빈 칸을 제거하여 암호 같은 문구를 만들어냅니다:
let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
switch character {
case "a", "e", "i", "o", "u", " ":
continue
default :
puzzleOutput += character
}
println(puzzleOutput)
}
// prints "grtmndsthnklk"
위 예제코드에서는 모음이나 빈 칸을 만났들 때 continue
키워드를 호출합니다. 그러면 그 때 돌던 이터레이션은 즉시 종료하고 다음번 이터레이션의 시작 지점으로 이동합니다. 이렇게 하면 switch
블록이 모음과 빈 칸일 때만 매치하도록 할 수 있습니다. 출력해야 하는 모든 문자를 다 case로 검토할 필요가 없죠.
break
문은 흐름제어문 전체를 즉시 종료시킵니다. break
문은 switch
문이나 루프문 안에서 사용할 수 있는데요, switch
문이나 루프문을 일찍 종료시키고 싶을 때 사용합니다.
break
는 루프문 안에서 쓰이면, 루프의 실행을 즉시 종료시키고, 루프를 닫는 중괄호(}
) 뒤에 나오는 코드로 코드 실행을 이동시킵니다. break
뒤에 있는 나머지 코드부분은 이터레이션을 돌지 않습니다. 그리고 그 다음 이터레이션은 시작되지 않습니다.
break
를 switch
문 안에서 사용하면, switch
문 전체를 즉시 종료시킵니다. 그리고 코드 실행을 switch
문이 끝나는 부분(}) 이후로 이동시킵니다.
switch
문에서 한 가지 이상의 경우(case)에 매치하는 경우를 무시해야 할 때, 사용할 수 있습니다.
Swift에서 switch
문은 가능한 모든 경우를 다루어야 하며, 각 경우는 모두 실행가능한 코드를 포함해야 합니다. 하지만 때로는 어떤 case에 해당하면 그 경우에는 아무것도 하지 않고 넘어가야 할 수도 있습니다. 이렇게 할 때 아무것도 하지 않고 넘어가야 하는 case의 본문에 break
문을 사용하면 됩니다. 그 case가 매치되는 경우, 본문에 있는 break
문이 switch
문의 시행을 즉시 종료시킵니다.
NOTE case 본문에 주석만 있는 경우에도 컴파일 에러가 납니다. 주석은 실행문이 아니므로, 그 case를 실행하지 않고 넘어가려면
break
문을 써야 합니다.
다음의 예제는 Character
변수의 값(Character value)이 무엇인가에 따라서, 이 값이 네 개 언어 중 하나에서 숫자를 나타내는지 여부를 판단합니다. 과감하게 몇 가지 값은 하나의 case와 매치되도록 했습니다:
let numberSymbol: Character = "三" // Simplified Chinese for the number 3
var possibleIntegerValue: Int?
switch numberSymbol {
case "1", "١", "一", "๑":
possibleIntegerValue = 1
case "2", "٢", "二", "๒":
possibleIntegerValue = 2
case "3", "٣", "三", "๓":
possibleIntegerValue = 3
case "4", "٤", "四", "๔":
possibleIntegerValue = 4
default:
break
}
if let integerValue = possibleIntegerValue {
println("The integer value of \(numberSymbol) is \(integerValue).")
} else {
println("An integer value could not be found for \(numberSymbol).")
}
// prints "The integer value of 三 is 3."
위 예제는 numberSymbol
이 라틴어, 아랍어, 중국어, 태국어의 숫자 1에서 4에 해당하는지 판단합니다. 매치하는 경우가 나타나면, switch
문은 possibleIntegerValue
에 해당하는 정수값을 넣습니다. 이때 possibleIntegerValue
는 선택적입니다. 즉 필수로 선언해야 하는게 아니라 case 본문 내에서 사용할 때에 선언하는 것이죠.
switch
문이 다 실행된 후에, 원하는 값을 찾았는지 판단하기 위해 바인딩을 사용합니다. 이 바인딩도 선택적으로 사용하는 것입니다. 변수 possibleIntegerValue
는 선택적 자료형이기 때문에 초기값으로 nil을 갖고 있습니다. nil
값을 명시적으로 넣어주지 않아도 암묵적으로 nil
값을 갖습니다. 따라서 possibleIntegerValue
가, 네 개 case중 어느 하나와 매치가 되어서, 어떤 실제 값을 갖게 되었을 때에야 바인딩이 일어날 것입니다.
위 예제에서 매치가 가능한 모든 문자를 case로 만드는 것은 너무 일이 많습니다. 그래서 default case를 두어서 매치되지 않은 문자에 대해서 다루도록 했습니다. 이 default case는 아무 작업도 하지 않아도 됩니다. 그래서 본문에 break
문만 두었습니다.
default
case와 매치한 경우에, break
문은 switch
문의 실행을 종료시키고, 코드 실행은 if let문으로 이동합니다.
Swift에서 switch
문은 한 case와 매치한 후에, 다음 case로 넘어가지 않습니다(fallthrough). 대신헤 한 case와 매치하고 그 본문에 있는 코드가 실행된 후에, switch
문 전체가 종료됩니다. 이와 달리 C언어에서는 fallthrough가 일어나지 않게 하려면 명시적으로 각 case 본문 끝부분에 break
를 써주어야 합니다. 이 점을 보면 C언어보다 Swift에서 switch
구문이 더욱 예측가능하다고 할 수 있습니다. 즉 실수로 의도하지 않았는데 case를 여러 개 실행시키는 것을 방지할 수 있습니다.
C언어에서 일어나는 fallthrough를 꼭 사용해야 한다면, fallthrough
키워드를 사용해서 필요할 때 사용할 수도 있습니다. 다음 예제는 어떤 수를 묘사하는 데에 fallthrough
를 사용합니다.
let integerToDescribe = 5
var description = "수 \(integerToDescribe) 는"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
description += "소수이며, 또한"
fallthrough
default :
description += " 정수입니다."
}
println (description)
//prints "수 5는 소수이며, 또한 정수입니다."
위 예제에서는 이름이 description
인 String
변수를 하나 만들어서 초기값을 넣습니다. 그런 다음 이 함수는 switch
문을 사용해서 integerToDescribe
의 값이 어느 경우에 해당하는지 검토합니다. integerToDescribe
의 값이 목록에 있는 소수에 해당하면, 함수는, ‘소수이며 또한’이라는 묘사부분을 description
변수의 끝에다가 붙입니다. 그런 다음 키워드 fallthrough
를 사용해서 default
case의 경우에 해당하는지도 봅니다. default
case는 추가적인 텍스트를 description
변수의 끝에 붙입니다. 이제 switch
문 실행이 완료되었습니다.
integerToDescribe
의 값이 목록 안에 있는 소수에 해당하지 않으면, 첫번째 case와는 매치하지 않습니다. 그런 다음 integerToDescribe
는 어떤 값이더라도 매치하는 default
case와 매치합니다.
switch
문 실행이 완료한 후에, 수에 대해 기술한 description
이 println
함수를 사용하여 출력됩니다. 위 예제에서 5
는 소수라고 확인되었습니다.
NOTE 키워드
fallthrough
를 사용할 때, 조건을 걸어서 다음으로 넘어갈 case를 지정할 수는 없습니다. C언어에서 처럼,fallthrough
키워드를 사용하면 단지, 다음에 나오는 case로 넘어갈 수 있을 뿐입니다.
Swift에서는 루프문이나 switch
문 안에서 루프문이나 switch
문을 중첩해서 사용함으로써 복잡한 흐름 제어구조를 만들 수도 있습니다. 그리고 루프문과 switch
문 안에서 break
문을 사용해서 이들을 바로 종료시킬 수도 있습니다. 이런 경우에 break
로 종료시키려는 것이 어느 루프문 또는 switch문인지 명시적으로 표시해주면 좋습니다. 마찬가지로, 여러 개의 루프문을 중첩했을 때 continue
문이 어느 루프문에 영향을 미치는지 명시적으로 표시해주면 좋습니다.
영향을 미칠 대상을 명시적으로 표시하기 위해서는, 루프문이나 switch
문에다가 구문 이름표(statement label)를 붙일 수 있습니다. 그리고 break
문이나 continue
문을 사용할 때 어느 것을 종료시킬지 구문 이름표를 붙임으로써 명시적으로 표시할 수 있습니다.
구문에다가 구문 이름표를 붙이는 방법은 구문의 introducer 키워드 앞에다가 이름을 쓰고 그 뒤에 콜론:
을 찍는 것입니다. 다음은 while
루프 안에서 구문 이름표를 사용한 예입니다. 이 사용방법은 다른 루프문이나 switch
문의 경우에도 동일합니다:
label name : while condition {
statements
}
다음 예제에서는 앞에서 다루었던 뱀과 사다리 게임을 약간 변형시킨 것인데, break
문과 continue
문을 사용할 때 구문 이름표가 붙은 while
루프를 사용합니다.
이번에는 게임 규칙이 하나 추가됩니다:
주사위를 던졌더니, 25번 칸 보다 넘어가는 경우에는, 25번 칸으로 이동할 수 있는 수가 나올 때까지 주사위를 다시 던져야 합니다.
게임판은 이전에 사용한 것과 동일합니다:
변수 finalSquare
, board
, square
, diceRoll
은 이전 예제에서와 동일한 값으로 초기화합니다:
let finalSquare = 25
var board = Int[](count : finalSquare + 1, repeatedValue: 0)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
var square = 0
var diceRoll = 0
이번 게임에서는 while
루프와 switch
문을 사용해서 게임을 구현합니다. while
루프의 구문 이름표는 gameLoop
라고 붙여서, 이 루프가 뱀과 사다리 게임 전체에 대한 주요 루프임을 나타냅니다.
while
루프의 조건부는 while squre != finalSquare
라고 하여 25번 칸에 정확하게 위치해야 한다는 규칙을 반영합니다:
gameLoop: while square != finalSquare {
if ++diceRoll == 7 { diceRoll = 1 }
switch square + diceRoll {
case finalSquare:
// 주사위에서 나온 수만 큼 이동해서 마지막 칸에 도달하면, 게임이 끝납니다.
break gameLoop
case let newSquare where newSquare > finalSquare:
// 주사위에서 나온 수만 큼 이동했을 때 마지막 칸을 넘어가면, 게임이 계속되고 주사위를 다시 던집니다.
continue gameLoop
default :
// 주사위에서 나온 수 만큼 이동합니다.
square += diceRoll
square += board[square]
}
println("Game over!")
루프가 돌 때마다 초반에 주사위가 던져집니다. 이 때 게이머를 바로 이동시키지 않고 switch
문을 사용해서 이동시켰을 때의 결과를 검토해서 이동시킬지 여부를 판단합니다:
break gameLoop
라고 쓴 부분은 코드 실행을 while
루프문 바깥에 있는 줄로 이동시키며, 게임을 끝냅니다. continue gameLoop
라고 쓴 부분은 현재의 while loop 이터레이션을 종료시키며, 루프의 다음번 이터레이션을 돌게 합니다. diceRoll
값 만큼 앞으로 이동합니다. 그리고 프로그램은 이동한 칸에 뱀이나 사다리가 있는지 확인합니다. 그런 다음 루프는 끝나고, 코드 실행은 while
조건부로 이동하여, 게임이 지속될지 여부를 판단합니다.NOTE 위 예제에서
break
문 안에gameLoop
라는 구문 이름표를 쓰지 않으면,while
문이 아니라switch
문에서만 빠져나오게 됩니다.gameLoop
라는 이름표를 사용하였기 때문에 어느 흐름제어문(control statement)가 종료되어야 하는지 명확해 집니다. 또한 엄밀히 말하면, 루프의 다음번 이터레이션으로 넘어가기 위해서continue gameLoop
를 호출할 때에, 반드시gameLoop
를 써주어야 하는 것은 아닙니다. 게임 내에서 루프는 하나 밖에 없기 때문에,continue
를 썼을 때 어느 루프의 다음 이터레이션으로 넘어갈지에 대해 애매모호함이 발생하지 않습니다. 그렇더라도, 이 때gameLoop
이름표를 사용해서 나쁠 것은 없습니다. 오히려 이렇게 써주면,break
문에서 이름표를 사용한 것과 일관성을 유지하는 효과가 있으며, 코드에서 게임의 규칙(game’s logic)을 읽어내고 이해하는데에 도움을 줍니다.
Translator : Quartet( ungsik.yun@gmail.com )
함수는 특정 일을 수행하는 자기 완결성(Self-contained)을 가진 코드들의 집합입니다. 함수의 이름을 지으면서 이 함수가 무엇을 하는지 식별하게 할 수 있습니다. 그리고 그 이름으로 함수를 "호출(Call)"하여 필요할때 함수의 일을 수행하게 만들 수 있습니다. 스위프트(Swift)의 함수 문법은 파라메터가 없는 C스타일의 함수에서부터 지역 파라메터와 파라메터 이름 각각에 대한 외부 파라메터를 가지고 있는 복잡한 오브젝티브-C 스타일의 함수까지 전부 표현할 수 있습니다. 파라메터는 기본 값을 가질수 있어 단순한 함수 호출에 쓰일수 있습니다. 또한 In-out 파라메터로서 변수를 넘겨 변수가 함수의 실행후에 파라메터가 변경되게 할 수도 있습니다. 파라메터 타입과 반환(Return) 타입으로 이루어진 모든 스위프트의 함수들은 타입을 가집니다. 스위프트에 있는 다른 타입들과 마찬가지로, 함수의 타입들을 사용할 수 있습니다. 즉 함수를 다른 함수에 파라메터로서 넘겨주거나 함수를 다른 함수에서 반환받을 수 있습니다. 함수들은 유용한 기능 캡슐화를 위해 중첩된 함수안의 범위 내에서 쓰여질수도 있습니다.
함수를 정의할때 함수의 입력(파라메터)을 하나 이상의 이름이 있고 타입이 정해진 값으로 할 수 있습니다. 또한 값의 타입은 함수의 실행이 끝났을때 함수가 되돌려줄 수 있습니다 (반환 타입).
모든 함수는 함수명을 가지고 있으며, 함수명은 함수가 하는일을 설명해줍니다. 함수를 사용하기위해서 함수를 함수의 이름을 사용하여 "호출"하고 함수의 파라메터 타입들과 일치하는 입력 값들(아규먼트Arguments)을 넘겨줍니다. 함수의 입력값은 함수의 파라메터리스트와 언제나 일치해야합니다.
아래의 함수 예제 이름은 greetingForPerson
입니다. 함수가 행하는 일이 바로 그것(사람에게 환영인사Greeting for person)이기 때문입니다. 입력으로 사람의 이름을 받아서 그 사람에 대한 환영 인사를 반환합니다. 이를 달성하기 위해 파라메터를 하나 정의하고 - personName
이라는 String
값 - 반환 타입을 String
으로 하여 사람에 대한 환영 인사를 하는 것입니다.
func sayHello(personName: String) -> String {
let greeting = "Hello, " + personName + "!"
return greeting
}
이 모든 정보들은 func
키워드 접두어를 쓰는 함수의 정의안에 포함이 되게됩니다. 함수의 반환 타입을 표시하기위해 함수 이름의 오른쪽에 화살표(하이픈과 우측 꺽괄호) ->
와 반환할 타입을 표시합니다.
함수 정의는 함수가 무엇을 하는지, 무엇을 파라메터로 받는지 완료되었을때 무엇을 반환하는지 설명합니다. 함수 정의는 함수가 코드안에서 호출될때 명확하고 애매함이 없는 방법으로 사용될수 있게합니다:
println(sayHello("Anna"))
// prints "Hello, Anna!"
println(sayHello("Brian"))
// prints "Hello, Brian!"
sayHello
함수를 괄호안에 String
타입의 인수를 넣어서 호출합니다. 예를들면 sayHello("Anna")
처럼 말이죠. sayHello
가 String
타입을 반환하기에 sayHello
함수는 println
로 싸여서 호출될 수 있습니다. 이렇게 함으로서 println
함수가 sayHello
함수의 반환값을 위에 보이는 것처럼 출력할 수 있습니다.
sayHello
함수의 몸체는 greeting
이라는 새 String
상수를 선언하는 것으로 시작합니다. 그리고 greeting
을 personName
에 대한 단순한 환영인사 메시지로 설정합니다. 이 환영 인사는 return
키워드를 통해 함수의 밖으로 되돌려지게 됩니다. return greeting
이 실행 되면 함수의 실행은 끝나게되고, greeting
의 현재 값을 돌려주게됩니다.
sayHello
함수를 다른 입력값으로 여러번 호출할 수 있습니다. 위의 예제는 입력값이 "Anna", "Brian" 일때를 각각 보여주고 있습니다. 함수는 사람(입력값)에 맞게끔 환영인사를 각각의 경우에 맞추어 돌려줍니다.
함수 몸체를 단순화하기 위해서는, 메시지의 생성과 반환을 한줄로 합치면 됩니다:
func sayHelloAgain(personName: String) -> String {
return "Helloagain, " + personName + "!"
}
println(sayHelloAgain("Anna"))
// prints "Helloagain, Anna!"
스위프트에서 함수 파라메터와 반환값은 극도로 유연합니다. 이름없는 파라메터를 사용하는 단순한 기능성 함수에서부터 명시적 파라메터 이름(expressive parameter names)과 다른 파라메터 옵션을 가진 복잡한 함수에 이르기까지 무엇이든 정의할수 있습니다.
함수는 괄호 안에서 콤마(,
)로 구분되는 복수의 입력 파라메터를 가질수 있습니다.
이 함수는 반개영역(half-open range)의 시작과 끝의 인덱스를 받아 얼마나 많은 요소(elements)들이 영역안에 있는지 계산합니다:
func halfOpenRangeLength(start: Int, end: Int) -> Int {
return end - start
}
println(halfOpenRangeLength(1, 10))
// prints "9"
함수에 입력 파라메터를 정의할 필요는 없습니다. 밑의 예제는 입력 파라메터가 없는 함수입니다. 이 함수는 호출될때마다 언제나 같은 메시지를 반환합니다.
func sayHelloWorld() -> String {
return "hello, world"
}
println(sayHelloWorld())
// prints "hello, world"
함수 정의는 아무런 파라메터를 받지 않는다고 해도 함수 이름뒤에 괄호를 포함해야 합니다. 함수가 호출될 때도 함수 이름뒤에 빈 괄호 한쌍을 표시해야 합니다.
함수에 반환 타입을 정의할 필요는 없습니다. 밑의 예제는 sayHello
의 waveGoodbye
라 불리는 버전입니다. 값을 반환하지 않고 자신의 String
값을 출력합니다.
func sayGoodbye(personName: String) {
println("Goodbye, ` \(personName)! ")
}
sayGoodbye("Dave")
// prints "Goodbye, Dave!"
반환값이 없기 때문에 함수 정의는 반환 화살표(->
)나 반환 타입을 포함하지 않습니다.
NOTE 엄밀히 말하자면,
sayGoodbye
함수는 반환값이 정의되어있지 않아도 여전히 반환값을 가집니다. 반환값이 정의되어있지 않은 함수는Void
타입의 특수값을 반환합니다.()
로 쓰여질수 있는 단순한 빈 튜플(Tuple)이며, 사실상 요소를 갖고있지 않은 튜플입니다.
함수가 호출되었을때 함수의 반환값은 무시될수 있습니다.
func printAndCount(stringToPrint: String) -> Int {
println(stringToPrint)
return countElements(stringToPrint)
}
func printWithoutCounting(stringToPrint: String) {
printAndCount(stringToPrint)
}
printAndCount("hello, world")
// prints "hello, world" and returns a value of 12
printWithoutCounting("hello, world")
// prints "hello, world" but does not return a value
첫번째 함수인 printAndCount
는 문자열을 출력하고 출력한 문자열의 캐릭터 갯수를 세서 Int 타입으로 반환합니다. 두번째 함수인 printWithoutCounting
은 첫번째 함수를 호출합니다. 하지만 반환값은 무시합니다. 두번째 함수가 호출되면 메시지는 첫번째 함수에 의해 여전히 출력되지만, 첫번째 함수의 반환값은 사용되지 않습니다.
NOTE 반환값은 무시될수 있습니다. 하지만 함수는 언제나 값을 반환할것입니다. 반환 타입이 정의된 함수는 값을 반환하지 않은채로 함수가 실행 될수 없습니다. 그렇게 하려고 시도할 경우 컴파일 에러를 낼 것입니다.
튜플 타입은 하나의 합성된 반환값으로서 함수의 반환에 사용될 수 있습니다.
아래의 예제는 count
라는 함수의 정의입니다. 이 함수는 아메리칸 영어에서 사용되는 표준 모음과 자음을 기반으로 모음과 자음 그리고 다른 문자들을 문자열에서 셉니다.
func count(string: String) -> (vowels: Int, consonants: Int, others: Int) {
var vowels = 0, consonants = 0, others = 0
for character in string {
switch String(character).lowercaseString {
case "a", "e", "i ", "o", "u":
++vowels
case "b", "c", "d", "f", "g", "h", "j", "k", "l ", "m",
"n", "p", "q", "r", "s", "t", "v", "w ", "x", "y", "z":
++consonants
default:
++others
}
}
return (vowels, consonants, others)
}
이 count
함수를 이용함으로서 임의의 문자열의 문자 갯수를 셀수 있습니다. 그리고 세 개의 이름있는 Int
값으로 구성된 튜플로 그 값을 받아옵니다.
let total = count("some arbitrary string! ")
println("\(total.vowels) vowels and \(total.consonants) consonants")
// prints "6 vowels and 13 consonants"
튜플의 멤버들은 함수 내에서 반환할때 이름을 지을 필요가 없습니다. 함수 정의시에 함수의 반환 타입에 이미 명시가 되어있기 때문입니다.
위의 모든 함수들은 함수 자신의 파라메터로 파라메터 이름을 정의하고 있습니다.
func someFunction(parameterName: Int) {
// function body goes here, and can use parameterName
// to refer to the argument value for that parameter
}
하지만 그러한 파라메터 이름들은 오직 함수 자신의 몸체(Body) 안에서만 사용될 수 있습니다. 또한 함수를 호출할때는 사용할 수 없습니다. 그러한 종류의 파라메터 이름은 지역 파라메터 이름(local parameter names)이라고 합니다. 오직 함수의 내부(Body)에서만 사용할 수 있기 때문입니다.
때때로 각각의 파라메터의 이름을 함수를 호출할때 지어주는 것이 유용할때가 있습니다. 함수에게 어떤 인수가 어떤 목적인지 지시하기 위해서죠. 함수 사용자에게 파라메터 이름을 제공하고 싶다면 지역 파라메터 이름과 외부 파라메터 이름을 정의하면 됩니다. 외부 파라메터 이름은 지역 파라메터 이름 바로 앞에 공백으로 구분해서 작성합니다.
func someFunction(externalParameterName localParameterName: Int) {
// function body goes here, and can use local ParameterName
// to refer to the argument value for that parameter
}
NOTE 만약 외부 파라메터 이름이 파라메터에 대해 제공된다면, 외부 파라메터 이름은 언제나 함수 호출시에 사용되어야 합니다.
예를 들어 다음과 같은 함수가 있다고 합시다. 이 함수는 두 문자열 사이에 joiner
문자열을 삽입해 연결하는 함수입니다.
func join(s1: String, s2: String, joiner: String) -> String {
return s1 + joiner + s2
}
이 함수를 호출할때 함수로 전달되는 세 문자열의 목적이 불분명합니다.
join("hello", "world", ", ")
// returns "hello, world"
문자열 값들의 목적을 명확하게 하기 위해, 외부 파라메터를 join
함수의 각각의 파라메터에 정의합니다.
func join(string s1: String, toString s2: String, withJoiner joiner: String)
-> String {
return s1 + joiner + s2
}
이 버전의 join 함수에서는 첫번째 파라메터의 외부 이름은 string
이며 지역 이름은 s1
이다. 두번째 파라메터는 외부 이름으로 toString
을 쓰고 지역 이름은 s2
이다. 그리고 세번째 파라메터는 외부 이름으로 withJoiner
를 쓰고 지역 이름은 joiner
이다.
이제 외부 파라메터 이름을 사용하여 함수를 호출할때 명확하고 애매하지 않은 방법으로 호출할 수 있게 되었다.
join(string: "hello", toString: "world", withJoiner: ", ")
// returns "hello, world"
외부 파라메터 이름의 사용은 이 두번째 join
함수를 명시적이며 말이 되는(sentence-like) 방법으로 사용자들이 호출할 수 있게 합니다. 함수 몸체는 여전히 가독성이 좋고 명확한 의도를 가진채로 말이죠. (while still providing a function body that is readable and clear in intent.)
NOTE 누군가가 코드를 처음 보았을때 명확하지 않을 수 있다면 외부 파라메터 이름을 쓰는것을 언제나 고려하십시오. 만약 함수가 호출될때 각각의 파라메터들의 목적이 명확하고 모호하지 않다면 외부 파라메터 이름을 정할 필요는 없습니다.
만약 함수의 외부 파라메터 이름을 제공하려 할때 이미 해당 파라메터의 내부 이름(local parameter name)이 이미 적절한 이름을 가지고 있다면, 똑같은 이름을 두번 쓸 필요가 없습니다. 대신 파라메터 이름을 한번 쓰고, 이름의 접두어로 해시 심볼(hash symbol) (#
)을 붙입니다. 이렇게 함으로서 스위프트는 해당 이름을 외부 파라메터 이름과 지역 파라메터 이름으로 동시에 쓰게 됩니다.
이 예제는 containsCharacter
함수를 정의하고 호출합니다. 해당 함수는 두 입력 파라메터에 #
을 붙여서 같은 이름으로 외부 파라메터 이름과 내부 파라메터 이름으로 쓰이게 했습니다.
func containsCharacter(#string: String, #characterToFind: Character) -> Bool {
for character in string {
if character == characterToFind {
return true
}
}
return false
}
이 함수의 파라메터 이름 선정은 함수 몸체를 명확하고 가독성있게 하며 동시에 함수 호출시에 모호함이 없게 했습니다.
let containsAVee = containsCharacter(string: "aardvark", characterToFind: "v")
// containsAVee equals true, because "aardvark" contai ns a "v"
함수 정의의 일부로서 파라메터의 기본 값을 지정해줄 수 있다. 기본값이 지정되어 있으면 함수를 호출할때 해당 파라메터를 생략할 수 있다.
NOTE 기본값을 가지는 파라메터는 함수의 파라메터 리스트에서 마지막에 둔다. 이렇게 함으로써 함수 호출이 기본값을 가지지 않는 파라메터들이 언제나 같은 순서임을 보장할 수 있고, 매번 함수가 호출될 때마다 같은 함수가 호출되게 한다.
이 함수는 앞서 보인 join
함수의 joiner
파라메터에 기본값을 부여한 버전입니다.
func join(string s1: String, toString s2: String, withJoiner joiner: String = " ")
-> String {
return s1 + joiner + s2
}
만약 join
함수의 joiner
문자열 값이 주어지면, 앞서 보았던 것처럼 해당 문자열 값이 두 문자열을 붙이는데 사용됩니다.
join(string: "hello", toString: "world", withJoiner: "-")
// returns "hello-world
하지만 아무런 값이 joiner
에 주어지지 않는다면, 기본값인 공백 한칸 (" "
)이 대신 사용됩니다.
join(string: "hello", toString: "world")
// returns "hello world"
대부분의 경우 외부 파라메터 이름에 기본값을 제공(외부 파라메터이기에 요구되기도 하는)하는 것은 유용하다. 그렇게 함으로써 함수가 호출될때 인수가 파라메터에 대해 가지는 목적이 명확해집니다.
이 과정을 쉽게 하기위해, 외부 이름을 부여하지 않은 파라메터에 대해 스위프트는 자동 외부 이름을 기본값이 정의되어 있는 파라메터에 대해 제공합니다. 자동 외부 이름은 앞서 본 해시 심볼(#
)을 사용한 것처럼, 지역 이름과 똑같은 이름이 됩니다.
여기에 joiner
문자열 값에 기본값을 부여하였지만, 파라메터 일체에 외부 파라메터 이름은 주지 않은 버전의 join
함수가 있습니다.
func join(s1: String, s2: String, joiner: String = " ") -> String {
return s1 + joiner + s2
}
이 경우에 스위프트는 자동적으로 외부 파라메터 이름을 기본값이 있는 파라메터 joiner
에 대해 부여합니다. 그러므로 외부 이름은 반드시 함수가 호출 될 때에 제공되어야 하며, 파라메터의 목적을 명확하고 모호하지 않게 합니다.
join("hello", "world", joiner: "-")
// returns "hello-world"
NOTE 함수를 정의할때 명시적인 외부 이름을 쓰는 것 대신에 밑줄(
_
)을 씀으로써 이 동작을 수행하지 않게 할 수 있습니다. 하지만 기본값을 가진 파라메터에 적절한 외부 이름을 제공하는것은 언제나 바람직합니다.
가변 갯수 파라메터는 특정 타입의 값을 0개 이상 받을 수 있습니다. 가변 갯수 파라메터를 사용함으로써 함수 호출시 입력 값들이 임의의 갯수가 될수 있다고 정할 수 있습니다. 파라메터의 타입 이름의 뒤에 마침표 세개(...
)를 삽입하는 것으로 가변 갯수 파라메터를 작성할 수 있습니다.
가변 갯수 파라메터로 함수의 내부에 전달된 값들은 적절한 타입의 배열(array
)로 만들어집니다. 예를 들어 numbers
라는 이름의 가변 갯수 파라메터의 타입이 Double...
이라면 함수의 내부에서는 Double[]
타입의 numbers
이름을 가진 배열로 만들어집니다.
밑의 예제는 평균이라 불리는 산술 평균(arithmetic mean)을 임의의 갯수를 가진 숫자의 목록에서 구합니다.
func arithmeticMean(numbers: Double...) -> Double {
var total : Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8, 19)
// returns 10.0, which is the arithmetic mean of these three numbers
NOTE 함수는 최대 한개의 가변 갯수 파라메터를 가질 수 있습니다. 그리고 가변 갯수 파라메터는 언제나 파라메터 목록의 마지막에 배치되어야 합니다. 이렇게 함으로써 복수의 파라메터를 가진 함수를 호출할때 생기는 모호함을 피할 수 있습니다. 만약 함수의 파라메터중 하나 이상의 파라메터가 기본값을 가지고, 그와 동시에 가변 갯수 파라메터를 가진다면 가변 갯수 파라메터는 기본 값을 가지는 파라메터의 맨 마지막에 두어야합니다.
함수의 파라메터들은 기본적으로 상수들입니다. 함수의 내부에서 파라메터의 값을 바꾸려 시도하는 것은 컴파일 에러를 냅니다. 이렇게 함으로써 실수로 파라메터가 바뀌지 않게 합니다.
하지만 때로는 함수가 파라메터의 값을 복사하여 다양하게 사용하는 것이 유용할때가 있습니다. 새로운 변수(variable)를 정의하지 않고 대신 가변 파라메터를 하나 이상 지정하여 함수 내부에서 사용할 수 있습니다. 가변 파라메터는 상수보다는 변수처럼 사용 가능하며, 함수가 이용하는 파라메터의 변경 가능한 사본을 제공합니다.
가변 파라메터를 정의하려면 파라메터의 이름 앞에 var
키워드를 접두어로 사용합니다.
func alignRight(var string: String, count: Int, pad: Character) -> String {
let amountToPad = count - countElements(string)
for _ in 1...amountToPad {
string = pad + string
}
return string
}
let originalString = "hello"
let paddedString = alignRight(originalString, 10, "-")
// paddedString is equal to "-----hello"
// originalString is still equal to "hello"
이 예제는 alignRight
라는 함수를 새로 정의하고 있습니다. 이 함수는 입력 문자열을 오른쪽 가장자리로 정렬하고 더 긴 출력 문자열을 만듭니다. 문자열의 왼쪽에 생긴 공간에는 정해진 채움 문자로 채워집니다. 이 예제에서는 "hello"라는 문자열이 "-----hello"로 변환되었습니다.
alignRight
함수는 입력 파라메터 string
을 가변 파라메터로 정의하고, string
이 지역 변수(variable)로서 사용될 수 있는 문자열 값으로 초기화 되며, 함수 내부에서 변경될 수 있습니다.
이 함수는 우측 정렬된 전체 문자열 안에 얼마나 많은 채움 문자가 string
의 왼쪽에 들어가야 할지 계산하는 것으로 시작합니다. 이 값은 지역 상수인 amountToPad
에 저장됩니다. 그리고 함수는 amountToPad
만큼 pad
문자를 기존 문자열의 왼쪽에 붙여넣고 그 값을 반환합니다. 이러한 문자열 변경 과정에서 string
가변 파라메터가 사용됩니다.
NOTE 가변 파라메터에 생긴 변화는 각각의 함수 호출이 끝난 뒤에는 남아있지 않습니다. 또한 함수의 외부에서는 보이지(visible)않습니다. 가변 파라메터는 함수 호출이 되는 동안만 유지됩니다.
위에 설명 된 것과 같이 가변 파라메터는 오직 함수 자신의 내부에서만 변경 될 수 있습니다. 만약 함수가 파라메터의 값을 변경하고 그 변경이 함수 호출이 종료된 후에도 계속되길 원한다면, 파라메터를 입출력 파라메터로 정의하면 됩니다.
입출력파라메터를 정의하기 위해서는 inout
키워드를 파라메터 정의의 시작점에 작성하면 됩니다. 입출력 파라메터의 값은 함수의 안으로 전달 되어, 함수에 의해 변경되고, 함수에서 다시 나와서 원래의 값을 대체합니다.
입출력 파라메터로 넘길 수 있는 값은 인수(argument)뿐입니다. 상수나 문자 값은 입출력 파라메터로 넘겨질 수 없습니다. 상수나 문자값은 변경될 수 없기 때문입니다. 인수를 입출력 파라메터로 넘길때 변수의 이름 바로 앞에 앰퍼샌드(&
)를 붙여서 이 파라메터가 함수에 의해 변경될 수 있음을 표시합니다.
NOTE 입출력 파라메터는 기본값을 가질 수 없습니다. 또한 가변 갯수 파라메터도
inout
으로 지정할 수 없으며var
나let
으로 표시될 수도 없습니다.
여기에 swapTwoInts
라는 함수 예제가 있습니다. 이 함수는 두개의입출력 정수(integer) 파라메터인 a
와 b
를 가지고 있습니다.
func swapTwoInts(inout a: Int, inout b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoInts
함수는 단순히 두 값을 교환하여 b
를 a
의 값으로 하고, a
를 b
의 값으로 합니다. 이 함수는 a
의 값을 임시 상수인 temporaryA
에 저장하고, b
의 값을 a
로 할당합니다. 그리고 temporaryA
의 값을 b
로 할당합니다.
swapTwoInts
함수는 두 Int
타입의 변수를 가지고 서로의 값을 교환하는 함수라고 할 수 있습니다다. 주의할것은 someInt
와 anotherInt
는 앰퍼샌드 접두어가 쓰여서 함수에 전달되었다는 것입니다.
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
println("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"
위의 예제는 someInt
와 anotherInt
가 함수 외부에서 정의되었음에도, 그 값이 swapTwoInts
함수에 의해 변경 되었음을 보여주고 있습니다.
NOTE 입출력 파라메터는 함수가 값을 반환하는 것이 아닙니다. 위의
swapTwoInts
예제는 반환 타입이나 반환값을 정의하고 있지 않습니다. 하지만someInt
와anotherInt
의 값을 변경하죠. 입출력 파라메터는 함수가 함수 밖의 범위(scope)에 영향을 끼칠 수 있는 또다른 방법입니다.
모든 함수들은 특유의 함수 타입을 가지고 있습니다. 함수 타입은 함수의 파라메터 타입들과 반환 타입으로 구성됩니다. 예를 들면 이렇습니다.
func addTwoInts(a: Int, b: Int) -> Int {
return a + b
}
func multiplyTwoInts(a: Int, b: Int) -> Int {
return a * b
}
이 예제는 addTwoInts
와 multiplyTwoInts
, 두개의 단순한 수학 함수를 정의합니다. 함수들은 각각 두개의 Int
값을 취하고 Int
값을 계산의 적절한 결과로서 반환합니다.
위 두 함수의 타입은 (Int, Int) -> Int
입니다. 이것을 "함수 타입은 Int
타입의 파라메터가 두개며 반환값의 타입은 Int
다." 라고 말할 수 있습니다.
여기의 또다른 예제는 파라메터나 반환값이 없는 함수입니다.
func printHelloWorld() {
println("hello, world")
}
이 함수의 타입은 ()->()
이며, "함수는 파라메터가 없고 Void
를 반환한다."고 할 수 있습니다. 반환값이 정해지지 않은 함수는 언제나 Void
를 반환하며, 이는 빈 튜플인 ()
로 표시될 수 있습니다.
함수 타입 역시 스위프트의 다른 타입들처럼 사용될 수 있습니다. 예를들어 함수 타입의 상수나 변수를 만들어서 적절한 함수를 할당할 수 있습니다.
var mathFunction: (Int, Int) -> Int = addTwoInts
위 코드는 "두개의 Int
값을 취하며 Int
값을 반환하는 함수 타입 mathFuntion
을 정의하고, 이 새로운 변수가 addTwoInts
함수를 참조(refer)하도록 한다."고 할 수 있습니다.
위에서 본 addTwoInts
함수는 mathFunction
변수와 같은 타입입니다. 따라서 스위프트의 타입 체커에 의해 할당이 허용되죠.
이제 mathFunction
을 이용해 할당된 함수를 호출할 수 있습니다.
println("Result: \(mathFunction(2, 3))")
// prints "Result: 5"
함수 타입이 아닌 변수가 그러하듯이, 일치하는 타입의 다른 함수 또한 같은 변수에 할당 될 수 있다.
mathFunction = multiplyTwoInts
println("Result: \(mathFunction(2, 3))")
// prints "Result: 6"
다른 타입과 마찬가지로, 함수를 상수나 변수에 할당할때 스위프트가 타입을 추론하게 내버려 둘 수 있다.
let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int페
(Int, Int) -> Int
와 같은 함수 타입을 파라메터 타입으로 함수에 이용할 수 있다. 이로서 함수 구현의 일부를 함수가 호출 될때 함수를 호출하는 쪽에 맡기는 것이 가능하게 된다.
이 예제는 위에서 가져온 수학 함수의 결과를 출력한다.
func printMathResult(mathFunction: (Int, Int) -> Int, a: Int, b: Int) {
println("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// prints "Result: 8"
이 예제는 세 개의 파라메터를 가지는 printMathResult
함수를 정의합니다. 첫번째 파라메터는 타입이 (Int, Int) -> Int
인 mathFunction
입니다. 함수 타입이 맞는 함수라면 인수로서 첫번째 파라메터에 어느것이나 넘길 수 있습니다. 두번째와 세번째 파라메터는 a
와 b
이며 둘 다 Int
타입입니다. 이 둘은 제공된 수학 함수의 두 입력값으로 사용됩니다.
printMathResult
함수가 호출되면 addTwoInts
함수와, 정수 값으로 3
과 5
를 넘깁니다. 이 함수는 제공받은 함수를 호출할때 3
과 5
를 이용합니다. 그리고 결과인 8
을 출력합니다.
printMathResult
의 역할은 적절한 타입의 수학 함수의 실행 결과를 출력하는 것입니다. 이 함수는 넘겨받는 함수의 구현이 실제로 무엇을 하는지 상관하지 않습니다. 오직 함수가 정확하게 일치하는 타입인지만 봅니다. 이로써 printMathResult
함수가 타입에 안전한 방식(type-safe way)으로 자기 기능의 일부를 호출자에게 넘길 수 있게 됩니다.
함수 타입을 다른 함수의 반환 타입으로 사용할 수 있습니다. 이는 완전한 함수 타입을 반환할 함수 정의의 반환 화살표 (->
)바로 뒤에 작성함으로서 할 수 있습니다.
다음 예제는 두개의 간단한 함수인 stepForward
와 stepBackward
를 정의하고 있습니다. stepForward
함수는 입력값보다 1이 더 큰 값을 반환하고 stepBackward
함수는 입력 값보다 1이 작은 값을 반환합니다. 두 함수의 타입은 모두 (Int) -> Int
입니다.
func stepForward(input: Int) -> Int {
return input + 1
}
func stepBackward(input: Int) -> Int {
return input - 1
}
여기 chooseStepFunction
함수가 있습니다. 이 함수의 반환 타입은 "(Int) -> Int
를 반환하는 함수"입니다. chooseStepFunction
은 backwards
불리언 파라메터에 따라 stepForward
함수와 stepBackward
함수중 하나를 반환합니다.
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
return backwards ? stepBackward : stepForward
}
chooseStepFunction
를 이용하여 어느 한쪽 방향으로 나아가는(증가 또는 감소하는) 함수를 얻을 수 있다.
var currentValue = 3
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function
앞서 한 예제들은 currentValue
변수에 따라 점진적으로 0이 되기 위해 필요한 증가나 감소 방향을 산출한다.currentValue
의 초기값은 3
이며, 이는 곧 currentValue > 0
은 true
를 반환하게 합니다. 그리고 chooseStepFunction
이 stepBackward
함수를 반환하게 합니다. 반환된 함수에 대한 참조(reference)는 moveNearerToZero
상수에 저장됩니다.
이제 moveNearerToZero
가 올바른 함수를 참조하게 되었기에, 0까지 세는데 이용할 수 있습니다.
println("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
println("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
println("zero!")
// 3...
// 2...
// 1...
// zero!
여기까지 이 챕터에서 마주친 모든 함수들은 모두 전역 범위(global scope)에 정의된 전역 함수의 예제였습니다. 또한 중첩된 함수라 불리는, 함수 내부에 다른 함수를 정의할 수 있습니다.
중첩 함수는 범위 밖의 세계에서 기본적으로 숨겨져 있습니다. 하지만 감싸고 있는 함수에 의해 여전히 이용될 수 있습니다. 감싸고 있는 함수는 중첩된 함수들을 반환하여 다른 범위에서 함수가 사용될 수 있게 할 수 있습니다.
위 예제의 chooseStepFunction
을 다음과 같이 중첩된 함수를 이용하여 재작성 할 수 있습니다.
func chooseStepFunction(backwards: Bool) -> (Int) -> Int {
func stepForward(input: Int) -> Int { return input + 1 }
func stepBackward(input: Int) -> Int { return input - 1 }
return backwards ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
println("\(currentValue)... ")
currentValue = moveNearerToZero(currentValue)
}
println("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!
Translator : inureyes (inureyes@gmail.com)
클로저는 사용자의 코드 안에서 전달되거나 사용할 수 있는 기능을 포함한 독립적인 블록(block)입니다. Swift에서의 클로저는 C 및 Objective-C 의 blocks와 유사하며, 다른 언어의 람다(lambda)와도 유사합니다. 클로저는 자신이 정의된 컨텍스트 (context) 로부터 임의의 상수 및 변수의 참조(reference) 를 획득 (capture)하고 저장할 수 있습니다. (주: 클로저의 경우 클로저 바로 밖의 scope 의 상수 및 변수에 접근할 수 있다는 이야기입니다) 이렇게 상수 및 변수를 제약하는 특징때문에 클로저라는 이름이 되었습니다. Swift는 획득 과정의 메모리 관리를 모두 제어해줍니다.
노트 "획득" 개념에 대해서 익숙하지 않아도 걱정하지 마세요. 아래의 값 획득하기 항목에서 자세히 다룰 것입니다.
함수 에서 소개된 전역 및 중첩 함수들은 사실 클로저의 특수한 경우들입니다. 클로저는 아래의 세가지 중 하나의 형태입니다.
Swift의 클로저 표현식은 일반적인 경우에 대한 간략하고 명확한 구문을 깨끗하고 명확한 스타일로 최적화와 함께 제공합니다. 이러한 최적화는 아래의 항목을 포함합니다.
중첩 함수에서 소개된 중첩 함수들은 더 큰 함수의 일부로서 동작하는 자체 포함된 코드 블럭을 명명하거나 정의하는 편리한 방법입니다. 그러나, 종종 완전한 선언이나 이름이 없는 더 짧은 버전의 함수같은 구조를 만드는 것이 유용할 때가 있습니다. 이는 다른 함수들을 하나 또는 그 이상의 인자로 받는 함수를 만들때 특히 그렇습니다.
클로저 표현식들은 인라인 클로저를 간단명료하고 집중적인 문법으로 작성하는 방법입니다. 클로저 표현식은 명확성과 의도를 잃지 않는 선에서 가장 간결한 형태로 클로저를 작성하기 위한 몇가지 문법 최적화를 제공합니다. 아래의 클로저 표현식 예에서 sort 함수를 각 단계마다 동일한 기능을 표현하지만 더 간결해지도록 몇가지 단계를 거쳐 개량하는 최적화를 소개합니다.
Swift 의 표준 라이브러리는 당신에 제공한 정렬 클로저(sorting closure)의 결과값에 기반하여 알려진 값들의 배열을 정렬하는 sort 라는 함수를 제공합니다. 정렬 과정이 끝나면, sort 함수는 원래와 동일한 타입(type) 및 크기를 갖지만 그 요소들은 올바르게 정렬된 새로운 배열을 반환합니다.
아래의 클로저 표현식 예는 string 값으로 구성된 배열을 알파벳 역순으로 정렬합니다.
이 배열이 정렬될 배열입니다:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sort
함수는 두 매개변수를 받습니다.
Bool
값을 돌려주는 클로저. 정렬 클로저는 만약 첫 값이 두번째 값보다 앞에 나와야 할 경우 true
를, 나머지 경우에는 false
를 반환합니다.이 예제는 String 값들의 배열을 정렬하므로, 정렬 클로저는 타입 (String, String) -> Bool
타입의 함수가 되어야 합니다.
정렬 클로저를 제공하는 한가지 방법은 정확한 타입과 함께 일반적인 함수를 작성하고, 이 함수를 sort
함수의 두번째 인자로 사용하는 방법입니다.
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed i s equal to ["Ewa", "Daniell a", "Chris", "Barry", "Alex"]
첫 번째 문자열 (S1)이 두 번째 문자열 (S2)보다 큰 경우, backwards
함수는 정렬된 배열에서 s1
이 s2
보다 먼저 나와야 함을 의미하는 true
를 반환합니다. string
안의 character
들의 경우, "더 크다"는 의미는 "알파벳에서 나중에 등장하는" 것을 의미합니다. 이는 글자 "B"가 글자 "A"보다 "더 크다"는 의미이며, 문자열 "Tom" 이 문자열 "Tim" 보다 크다는 의미입니다. 따라서 이 함수는 "Barry"가 "Alex"보다 앞에 오게 되는 알파벳 역순 정렬 결과를 주게 됩니다.
그러나, 이것은 본질적으로 하나의 표현 함수 (a > b)
인 기능을 작성하기엔 다소 장황한 방법입니다. 이 예제의 경우 클로저 표현식 문법을 사용하여 인라인 정렬 클로저를 작성하는 것이 더 바람직할 것입니다.
클로저 표현식 문법은 아래의 일반 형식을 따릅니다:
{ ( parameters ) -> return type in
statements
}
클로저 표현식 문법은 상수 인자, 변수 인자 및 inout
인자를 사용할 수 있습니다. 기본 값은 주어지지 않습니다. 만약 당신이 가변 인자에 이름을 주고 마지막 매개 변수에 위치할 경우 가변 인자도 사용할 수 있습니다. 튜플 또한 인자 타입 및 반환 타입으로 사용할 수 있습니다.
아래의 예는 앞에서 소개한 backwards
함수의 클로저 표현 판입니다.
reversed = sort(names, { (s1: String, s2: String) -
> Bool in
return s1 > s2
})
이 인라인 클로저에 대한 인자의 및 리턴 타입의 정의는 backwards
함수의 정의와 동일합니다. 두 경우 모두, (s1: String, s2: String) -> Bool
로 쓸 수 있습니다. 그러나, 인라인 클로저 표현식의 경우, 인자와 리턴 타입은 중괄호 안에 쓰여야 하며, 밖에 쓰일 수는 없습니다.
클로저의 내용은 in
키워드로 시작합니다. 이 키워드는 클로저의 인자 및 반환 타입 정의가 끝났으며, 클로저의 내용이 시작됨을 지시합니다.
클로저의 내용이 매우 짧기 때문에, 심지어 한 줄에도 쓸 수 있습니다.
reversed = sort(names, { (s1: String, s2: String) - > Bool in return s1 > s2 } )
이 구문은 sort
함수의 전체적인 호출이 똑같이 유지됨을 보여줍니다. 괄호쌍은 여전히 함수의 전체 인자를 감싸고 있습니다. 그러나 그 중 하나의 인자는 이제 인라인 클로저입니다.
정렬 클로저가 함수의 인자로 전달되기 때문에, Swift는 클로저의 인자 타입과 sort
함수의 두번째 인자의 타입으로부터 반환되는 값의 타입을 유추할 수 있습니다. 이 인자는 (String, String) -> Bool
타입의 함수를 기대합니다. 이는 String, String
및 Bool
타입이 클로저 표현형의 정의의 일부로 쓰일 필요가 없음을 의미합니다. 모든 타입이 유추 가능하기 때문에, 반환 화살표 (->) 와 인자 이름을 감싼 괄호 또한 제외할 수 있습니다.
reversed = sort(names, { s1, s2 in return s1 > s2 } )
인라인 클로저 표현 형태로 클로저를 함수에 전달할 경우 인자와 반환 값의 타입을 유추하는 것이 언제나 가능합니다. 결과적으로, 인라인 클로저를 최대한의 형태로 명시적으로 기술할 일은 거의 없을 것입니다.
그럼에도 불구하고, 당신이 필요로 하거나, 또한 코드를 읽는 사람들에게 모호함을 주고 싶지 않을 경우 타입을 명시적으로 기술할 수 있습니다. sort
함수의 경우, 클로저의 목적은 정렬이 일어난다는 사실로부터 명확하게 보이며, 독자들은 문자열들의 배열을 정렬하는 것을 돕기 떄문에 이 클로저가 String
값과 함께 돌아간다고 가정하는 것이 안전합니다.
단일 표현식 클로저는 앞의 예에서 정의할 때 return
키워드를 생략하여 단일 표현식의 결과를 암시적으로 반환할 수 있습니다.
reversed = sort(names, { s1, s2 in s1 > s2 } )
sort 함수의 두번째 인자의 함수 형은 클로저가 Bool
값을 반드시 반환해야 함을 명확하게 해 줍니다. 클로저의 내용이 Bool
값을 반환하는 단일 표현식 (s1 > s2)
이기 때문에, 이 경우 애매모호함이 발생하지 않으며, return
키워드는 생략이 가능합니다.
Swift는 자동으로 단축 인자 이름을 인라인 클로저에 제공하며, 클로저의 인자들을 $0, $1, $2 등등의 방식으로 참조할 수 있습니다.
만약 이러한 단축 인자 이름들을 클로저 표현식에서 사용할 경우, 클로저 인자 리스트를 클로저의 정의에서 생략할 수 있으며, 단축 인자들의 번호 및 타입은 기대되는 함수 타입으로부터 추정될 것입니다. 클로저 표현식이 클로저 내용에 완전히 표현될 수 있으므로 in
키워드 또한 생략할 수 있습니다:
reversed = sort(names, { $0 > $1 } )
여기서 $0 와 $1 은 클로저의 첫번째와 두번째 String
매개변수를 가리킵니다.
사실 위의 클로저 표현식은 더 짧아질 수도 있습니다. Swift 의 String
타입은 String
에 특화된 크기 비교 연산자 (>) 를 두 String
인자를 갖는 함수로 정의하고 있으며, Bool
타입을 반환합니다. 이 연산자는 sort
함수의 두번째 인자를 위해 필요한 함수형과 정확히 일치합니다. 그러므로, 이 크기 비교 연산자를 바로 전달하면 Swift 는 사용자가 String
전용의 구현체를 사용하려고 한다고 유추합니다.
reversed = sort(names, > )
연산자 함수에 대해 더 많은 내용은 연산자 함수 항목을 참조하시기 바랍니다.
만약 클로저 표현식을 함수에 함수의 마지막 인자로 전달할 때 클로저 표현식이 긴 경우, 대신에 후행 클로저 (Trailing closure) 를 작성하는 것이 유용할 수 있습니다. 후행 클로저는 함수 호출 괄호의 밖 (또는 뒤) 에 쓰여져서 함수를 지원하는 클로저 표현식입니다.
func someFunctionThatTakesAClosure(closure: () -> ()) {
// function body goes here
}
// here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure({
// closure's body goes here
})
// here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
노트 클로저 표현식이 함수의 하나뿐인 인자이며 이 표현식을 후행 클로저로 작성할 경우, 함수를 호출할때 함수 이름의 뒤에 괄호쌍 () 을 쓸 필요는 없습니다.
위의 클로저 표현식 문법의 문자열 정렬 클로저는 sort
함수의 괄호 밖에 후행 클로저로 작성될 수도 있습니다.
reversed = sort(names) { $0 > $1 }
후행 클로저는 클로저가 충분히 길어서 줄 안이나 한 줄 정도로 기술할 수 없는 경우에 아주 유용합니다. 예를 들어, Swift의 Array
타입은 클로저 표현식을 하나의 인자로 받는 map
메소드를 제공합니다. 클로저는 행렬 안의 각 아이템마다 한 번씩 호출되고, 그 아이템의 (다른 타입일 수도 있는) 새롭게 매핑된 값을 반환합니다. 매핑의 동작과 반환값의 타입은 클로저에 의하여 지정됩니다.
map
메소드는 제공된 클로저를 각 행렬 항목마다 적용한 후, 새롭게 매핑된 값들이 원래 행렬의 해당 값들의 순서와 같도록 배치된 새 행렬을 반환합니다.
Int
값들로 구성된 행렬을 String
값들로 구성된 행렬로 변환하는 map 메소드를 후행 클로저와 함께 사용하는 예를 보겠습니다. 행렬 [16,58,510]
이 새로운 행렬인 ["OneSix", "FiveEight", "FiveOneZero"]
를 생성하기 위해 사용되었습니다.
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
위의 코드는 정수와 그 수들의 영어 표현사이의 매핑 사전을 생성합니다. 또한 문자열로 변환될 정수 행렬도 정의합니다.
이제 numbers
행렬을 map
메소드에 후행 클로저를 클로저 표현식으로 전달하는 방법으로 String
값의 행렬을 생성하기 위해 사용할 수 있습니다. map
메소드가 단 하나의 인자만을 받으므로 numbers.map
을 호출할 때 map
뒤에 어떤 괄호도 필요하지 않음을 기억하세요. 후행 클로저가 이 인자로 제공됩니다.
let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings is inferred to be of type String[]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map
함수는 클로저 표현식을 각 행렬의 항목마다 호출합니다. 클로저의 입력 인자, number
, 의 타입을 지정할 필요는 없는데, 이는 타입을 매핑될 배열의 값으로부터 추측할 수 있기 때문입니다.
이 예제에서, 클로저의 number
인자는 상수 및 변수 파라미터 에서 설명한 변수 인자 (variable parameter) 로 정의되었으므로, 인자의 값이 새로운 지역 변수를 정의하고 number
값을 그 변수에 할당하는 방법 대신 클로저 본문에서 변경될 수 있습니다. 또한 클로저 표현식은 매핑된 결과 배열의 타입을 지시하기 위해 String
의 반환 타입을 지정합니다.
클로저 표현 식은 호출될 때 마다 output
이라는 문자열을 생성합니다. number
의 마지막 숫자를 나머지 연산자 (number % 10)
를 사용하여 알아낸 후, 이 숫자에 해당되는 문자열을 digitNames
사전에서 찾습니다.
노트
digitNames
사전의 첨자 (subscript) (주: 적당한 표현이 없어서 의미적으로 가까운 '첨자'로 번역)에 접근할 때 느낌표 (!) 가 붙는데, 이 이유는 사전의 첨자 반환값은 사전 안에 해당되는 키값이 없어서 열람이 실패했을 경우 반환하는 선택적인 값이 있기 때문입니다. 위의 예에서,number % 10
은digitNames
사전을 위해 언제나 유효한 첨자 키값을 제공하는 것이 보장되어 있으므로, 느낌표는 첨자의 선택적인 반환 값에 보관된 값을 강제로 풀어String
값을 알기 위해 사용합니다.
digitNames
사전으로부터 가져온 문자열은 output
의 앞부분에 추가되며, 숫자의 역순으로 해당되는 문자열이 효율적으로 만들어집니다. ( number % 10
표현식은 16의 경우 6, 58의 경우 8, 510일 경우 0을 돌려줍니다.)
number
변수는 이후 10으로 나눠집니다. 이 변수가 정수이기 때문에, 나누기 과정에서 소숫점 아랫 값이 버려집니다. 따라서 16은 1이, 58은 5가, 510은 51이 됩니다.
이 과정은 number /=10
이 0이 될 때까지 반복되며, 이때 output
문자열이 클로저로부터 반환되고, map
함수에 의하여 출력 행렬에 더해집니다.
이 예제에서 사용한 후행 클로저 구문은 전체 클로저를 map
함수의 외부 괄호로 전체 클로저를 감쌀 필요 없이, 클로저의 기능을 클로저가 지원하는 함수의 바로 뒤에서 깔끔하게 캡슐화합니다.
클로저는 자신이 정의된 주변 컨텍스트로부터 상수 및 변수의 값을 획득할 수 있습니다. 클로저는 이러한 상수와 변수들을 원래 상수와 변수들이 정의된 범위 (scope) 가 더이상 존재하지 않는 경우에조차도 값을 참조하거나 수정할 수 있습니다.
Swift에서 클로저의 가장 간단한 형태는 다른 함수의 본문 안에 작성된 중첩 함수입니다. 중첩 함수는 바깥 함수의 모든 인자를 획득할 수 있으며, 또한 바깥 함수 내에서 정의된 모든 상수 및 변수를 획득할 수 있습니다.
아래는 Incrementor
라는 중첩 함수를 포함한 makeIncrementor
예입니다. 중첩된 incrementor
함수는 runningTotal
및 amount 의 두 값을 자신을 둘러싼 컨텍스트로부터 획득합니다. 이 두 값을 획득한 후, incrementor
는 호출될 때 마다 runningTotal
을 mount
만큼 증가시키는 클로저로써 makeIncrementor
로부터 반환됩니다.
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
makeIncrementor
의 반환 타입은 () -> Int
입니다. 이는 간단한 값 대신 함수를 반환함을 의미합니다. 반환되는 함수는 인자가 하나도 없으며, 호출될 때 마다 Int
값을 반환합니다. 어떻게 함수가 다른 함수를 반환할 수 있는가에 대해서는 반환 타입으로서의 함수 타입 을 참조하시기 바랍니다.
makeIncrementor
함수는 runningTotal 정수 변수를 정의하며, 이 변수는 현재 실행중인 incrementor 의 총합을 보관하고 반환될 변수입니다. 이 변수는 0으로 초기화됩니다.
makeIncrementor
함수는 Int
인자를 외부 이름인 forIncrement
와 지역 이름인 amount
로 받습니다. 이 인자로 전달된 인수는 runningTotal
이 incrementor
함수가 호출될 때 마다 얼마만큼씩 증가해야 할 지 지정합니다.
makeIncrementor
는 incrementor
라는 실제 증가를 수행하는 중첩 함수를 정의합니다. 이 함수는 간단하게 amount 를 runningTotal 에 더하고, 결과값을 반환합니다.
고립된 상황을 생각해보면, 중첩함수 incrementor
는 독특하게 보입니다.
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
incrementor
함수는 아무 인자도 갖고 있지 않으며, runningTotal
및 amount
를 함수 내에서 참조합니다. 이 함수는 자신을 둘러싼 함수로부터 runningTotal
및 amount
를 획득하고 함수 안에서 그 값들을 사용합니다.
이 함수는 amount
값을 수정하지 않기 때문에, incrementor
는 amount
안에 보관된 값을 획득하고 그 복사판을 보관합니다. 이 값은 새로운 incrementor
함수에서도 계속 이어져 보관됩니다.
그러나, 이 함수가 runningTotal
변수를 호출시마다 변경하기 때문에, incrementor
는 현재 runningTotal
의 복사본 대신 값의 참조를 획득합니다. 참조 획득은 runningTota
l 이 makeIncrementor
가 끝난 이후에도 사라지지 않음을 보증하며, incrementor
함수가 이후 호출될 때도 연속적으로 사용될 수 있음을 보증합니다.
노트 Swift는 어떤 변수가 참조 로 획득되고 어떤 변수가 복사값으로 획득될지 판단합니다. 사용자는
amount
및runningTotal
이incrementor
중첩 함수에서 쓰일지의 여부를 명기할 필요가 없습니다. Swift는 또한runningTotal
이 더이상incrementor
함수로부터 필요로하지 않을 때 폐기하는 모든 메모리 관리 과정을 담당합니다.
makeIncrementor
의 사용 예입니다.
let incrementByTen = makeIncrementor(forIncrement: 10)
이 예제는 호출될 때 마다 runningTotal
에 10씩을 더하는 증가 함수를 참조하는 incrementByTen
을 정의합니다. 이 함수를 여러번 부르면 동작을 볼 수 있습니다.
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
만약 새로운 incrementor
를 생성할 경우, 그 incrementor
는 새롭고 독립적인 runningTotal
변수로의 참조를 갖게 됩니다. 아래의 예제에서, incrementBySeven
은 새로운 runningTotal
변수의 참조를 획득하며, 이 변수는 incrementByTen
에서 획득한 변수와 연결되지 않습니다.
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40
노트 만약 클로저를 클래스 인스턴스의 프로퍼티로 지정하고, 클로저가 인스턴스 또는 인스턴스의 멤버를 참조하여 인스턴스를 획득할 경우, 클로저와 인스턴스의 강력한 참조 순환을 만들게 됩니다. Swift는 이러한 강력한 참조 순환을 깨기 위하여 캡처 리스트 (capture list) 를 사용합니다. 더 많은 정보는 클로저의 강력한 참조 순환 을 참조하시기 바랍니다.
위의 예에서, incrementBySeven
및 incrementByTen
은 상수입니다. 그러나 클로저로써 이러한 상수들은 여전히 그들이 획득한 runningTotal
변수를 증가시킬 수 있습니다. 이는 함수와 클로저가 참조 타입이기 때문입니다.
함수나 클로저를 상수에 할당하는 것은, 실제로는 그 상수에 함수나 클로저를 가리키는 참조를 할당하는 것입니다. 위의 예에서, incrementByTen
이 참조하는 것은 클로저를 가리키는 상수이며, 클로저 그 자체의 내용은 아닙니다.
이는 또한 클로저를 두 개의 다른 상수나 변수에 할당하면, 두 상수나 변수들이 동일한 클로저를 참조하게 되는 것을 의미합니다.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
Translator : inureyes (inureyes@gmail.com)
열거형 (Enumeration) 은 관련있는 값들의 그룹에 대한 일반적인 타입을 정의하며, 이를 이용하여 코드 안에서 타입에 안전한 방법으로 작업할 수 있습니다. C에 익숙한 사용자라면, C 열거형은 관련있는 이름을 정수값의 집합(set)에 할당하는 것을 알고 있을 것입니다. Swift의 열거형은 훨씬 더 유연하며, 열거형의 각 숫자마다 반드시 값을 제공할 필요가 없습니다. 만약 ("원시(raw)" 값으로 알려진) 값이 각 열거형 번호마다 제공될 경우, 그 값들은 문자열, 글자, 어떠한 정수나 부동 소수점 타입이 될 수 있습니다.
또한, 열거형 멤버들은 각각 다른 멤버 값에 대하여 다른 언어의 공용체(union)및 비슷한 기능들이 하듯 연관된 값들을 어떤 타입이든 지정할 수 있습니다. 관련있는 멤버들의 일반적인 집합을 하나의 열거형의 부분으로 정의할 수도 있으며, 각각은 그에 연관된 적당한 타입의 값들의 다양한 집합을 가질 수 있습니다.
Swift의 열거형은 열거형의 현재 값에 대한 추가적인 정보를 제공하기 위한 계산된 프로퍼티나, 열거형이 표현하는 값들과 연관된 기능들을 제공하는 인스턴스 메소드 같이 전통적으로 클래스 등에서만 지원되는 많은 기능들을 차용하였습니다. 또한 열거형은 초기 멤버 값을 제공하는 이니셜라이저(initiailizer)를 제공할 수 있고, 원래 구현을 넘어서 기능을 확장할 수도 있으며, 표준 기능을 제공하기 위한 프로토콜을 따를 수 있습니다.
이러한 기능에 대한 자세한 내용은 속성, 메소드, 초기화, 확장, 및 프로토콜을 참조하십시오.
열거형은 enum
키워드로 작성하며, 중괄호 안에 모든 정의를 집어넣습니다.
enum SomeEnumeration {
// enumeration definition goes here
}
여기에 나침반의 4가지 주요 방향을 위한 예제가 하나 있습니다:
enum CompassPoint {
case North
case South
case East
case West
}
(North
, South
, East
및 West
같이) 열거형에 정의된 값들은 이 열거형의 멤버 값들입니다. case
키워드는 멤버 값들의 새 줄이 정의될 것임을 나타냅니다.
NOTE C 및 Objective-C 와는 다르게, Swift의 열거형 멤버들은 생성시 기본 정수값들에 할당되지 않습니다. 위의 CompassPoints 예제에서 보듯, North, South, East 및 West는 명시적으로 0, 1, 2 및 3에 대응되지 않습니다. 대신에, 기본 열거형 멤버들은 CompassPoint의 명시적으로 정의된 타입과 함께 정의된 완벽하게 갖춰진 값입니다.
여러 멤버 값들이 콤마(,) 로 구분되어 한 줄에 나올 수도 있습니다:
enum Planet {
case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
각 열거형 정의들은 새로운 타입을 정의합니다. Swift의 다른 타입과 마찬가지로, 이름들 ( CompassPoint 및 Planet과 같은) 은 대문자로 시작해야 합니다. 자명하게 읽힐 수 있도록 열거형 타입에게 복수형 대신 단수형 이름을 주세요.
var directionToHead = CompassPoint.West
directionToHead 타입은 CompassPoint의 가능한 값들 중 하나가 초기화 될 때 유추됩니다. directionToHead가 CompassPoint로 선언되면, 짧은 닷 구문을 사용하여 그 값을 다른 CompassPoint 값으로 할당할 수 있습니다:
directionToHead = .East
directionToHead의 타입은 이미 알려져 있으므로, 값을 설정할 때 타입을 명기하지 않을 수 있습니다. 이러한 부분은 명시적으로 타입된 열거형 값들로 작업할 때 매우 읽기 편한 코드를 만들어줍니다.
각각의 열거형 값들을 switch
구문과 대응할 수 있습니다.
directionToHead = .South
switch directionToHead {
case .North:
println("Lots of planets have a north")
case .South:
println("Watch out for penguins")
case .East:
println("Where the sun rises")
case .West:
println("Where the skies are blue")
}
// prints "Watch out for penguins"
이 코드는 다음과 같이 읽을 수 있습니다: "directionToHead의 값을 봅시다. 만약 .North와 값이 같다면, "Lots of planets have a north" 를 출력합니다. 만약 .South와 값이 같다면, "Watch out for penguins" 를 출력합니다."
...식이 됩니다.
제어 구문 에서 설명했듯이, switch
구문은 열거형 멤버를 고려할때 완벽하게 작성되어야 합니다. 만약 .West를 표현하기 위한 case
가 빠진 경우, 이 코드는 CompassPoint 멤버의 완벽한 리스트를 고려하지 않았기 때문에 컴파일되지 않을 것입니다. 완벽성 (exhaustiveness) 의 요구는 열거형 멤버가 실수로 생략되는 것을 방지합니다.
모든 열거형 멤버에 대한 케이스를 제공하기에 적당하지 않은 경우, 명시적으로 언급되지 않은 멤버들을 위한 기본 케이스를 제공할 수 있습니다.
let somePlanet = Planet.Earth
switch somePlanet {
case .Earth:
println("Mostly harmless")
default:
println("Not a safe place for humans")
}
// prints "Mostly harmless”
앞 섹션의 예제는 열거형의 멤버들이 각각의 어떻게 정의되었는지 보여줍니다. 상수 및 변수를 Planet.Earth 에 할당할 수 있으며, 나중에 값들을 확인할 수도 있습니다. 그렇지만, 종종 멤버 값들과 함께 연관된 다른 타입의 값들을 저장하는 것이 유용한 경우들이 있습니다. 이는 추가적인 사용자 지정 정보를 멤버 값들마다 저장할수 있게 하며, 코드 안에서 멤버를 사용할 때 마다 정보가 변경되는 것을 허용합니다.
어떤 특정한 타입의 관련 값을 저장하는 Swift 열거형을 정의 할 수 있으며, 필요한 경우에 열거형의 각 멤버에 따라 값의 형식은 다를 수 있습니다. 이러한 열거형과 유사한 경우들이 다른 언어에서는 차별된 공용체 (discriminated union), 태깅된 공용체 (tagged unions) 및 변형체 (variants) 로 알려져 있습니다.
예를 들어 재고 추적 시스템이 각 제품을 두가지 타입의 바코드로 추적할 필요가 있다고 해 봅시다. 어떤 제품들은 UPC-A 포맷의 0에서 9 사이의 숫자를 사용하는 1차원 바코드로 레이블링 되어 있습니다. 각 바코드는 열 개의 "확인 번호(identifier)" 숫자가 뒤따르는 "번호 시스템" 숫자를 갖고 있습니다. 이 숫자들 뒤에는 각 코드가 제대로 스캔되었는지를 검증하기 위한 "확인(check)" 숫자가 붙습니다.
다른 제품들은 모든 ISO 8859-1 문자를 사용할 수 있으며 2,953글자의 길이를 갖는 QR 코드 포맷의 2차원 바코드로 레이블링되어 있습니다.
재고추적 시스템이 UPC-A 바코드를 3개의 숫자 튜플로 저장하고, QR 코드는 임의의 길이의 문자열로 저장할 수 있다면 매우 편할 것입니다.
Swift에서, 각 유형의 제품의 바코드를 정의하는 열거형은 다음처럼 보일 것입니다:
enum Barcode {
case UPCA(Int, Int, Int)
case QRCode(String)
}
이 코드는 다음과 같이 읽을 수 있습니다:
"(Int, Int, Int)
타입의 UPCA 값 또는 String
타입의 QRCode 값을 가질 수 있는 Barcode라는 열거형 타입을 정의합니다."
이 정의는 어떠한 실제 Int
및 String
값을 제공하지 않습니다. 오직 바코드 상수 및 변수들이 Barcode.UPCA 또는 Barcode.QRCode 중 하나와 같을 때, 그와 연관된 값들의 타입만을 정의합니다.
이제 새 바코드는 두가지 타입 중 하나로 생성될 수 있습니다:
var productBarcode = Barcode.UPCA(8, 85909_51226, 3)
이 예제는 productBarcode 라는 새 변수를 생성하고, Barcode.UPCA 의 값으로 (8, 8590951226, 3) 튜플 값을 배정합니다. 제공된 "식별자" 값은 바코드로 읽기 좋도록 정수 표현 안의 밑줄 -85909_51226-로 을 갖고 있습니다.
동일한 제품이 다른 형태의 바코드로 배정될 수도 있습니다.
productBarcode = .QRCode("ABCDEFGHIJKLMNOP")”
이 경우, 원래 Barcode.UPCA 및 정수 값은 새로운 Barcode.QRCode 와 문자열 값으로 대체됩니다. Barcode 타입의 상수 및 변수들은 .UPCA 또는 .QRCode 중 하나를 (해당되는 값들과 함께) 저장할 수 있지만, 한번에 둘 중 하나만 저장할 수 있습니다.
서로 다른 바코드 타입들은 앞에서와 같이 switch
구문을 사용하여 체크할 수 있습니다. 그러나, 이번 경우 관련된 값들은 스위치 구분의 일부로 추출될 수 있습니다. 각각의 연관 값들을 switch
의 case
내용으로 사용하기 위하여 (let
접두사와 함께) 상수 또는 (var
접두사와 함께) 변수로 추출할 수 있습니다.
switch productBarcode {
case .UPCA(let numberSystem, let identifier, let check):
println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case .QRCode(let productCode):
println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."
만약 열거형 멤버들의 모든 연관 값들이 상수로 추출되었거나 모두 변수로 추출되었다면, 간결함을 위하여 멤버 이름 앞에 하나의 var
또는 let
을 붙일 수 있습니다:
switch productBarcode {
case let .UPCA(numberSystem, identifier, check):
println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
case let .QRCode(productCode):
println("QR code with value of \(productCode).")
}
// prints "QR code with value of ABCDEFGHIJKLMNOP."
연관값들을 사용한 바코드 예제는 어떻게 열거형의 멤버들이 그들이 저장하는 여러 타입의 관련된 값들을 선언하는지에 대해 보여주었습니다. 연관 값들에 대한 다른 방법으로, 열거형 멤버들은 (원시 값들이라고 부르는) 모두 같은 타입인 기본값들로 미리 채워질 수 있습니다.
아래는 원시 ASCII 값들을 이름붙은 열거형 멤버들에 저장하는 예입니다.
enum ASCIIControlCharacter: Character {
case Tab = "\t"
case LineFeed = "\n"
case CarriageReturn = "\r"
}
여기서 ASCIIControlCharacter 열거형을 위한 원시 값들은 Character
타입이 되도록 정의되었으며, 더 일반적인 ASCII 제어 문자들로 할당되었습니다. Character
값들은 문자열 및 글자 에 설명되어 있습니다.
원시 값들은 연관된 값들과 같지 않음을 유의하세요. 원시 값들은 위의 세가지 ASCII 코드들처럼 코드 안에서 처음 열거형을 정의할 때 미리 정의된 값들입니다. 개개의 열거형 멤버들의 원시 값은 언제나 동일합니다. 연관 값들은 새 상수 또는 변수를 열거형의 멤버 중 하나에 기초하여 생성할 때 할당되며, 무엇을 하느냐에 따라 매번 다를 수 있습니다.
원시 값은 문자열, 글자, 정수 또는 어떠한 부동 소수점 타입이 될 수 있습니다. 각각의 원시 값은 열거형 정의 안에서 반드시 유일해야 합니다. 원시 값으로 정수가 사용되었다면, 열거형 멤버의 일부에 아무 값도 설정되지 않은 경우 자동 증가(Auto-incrementation)할 것입니다.
아래의 열거형은 태양으로부터의 순서를 원시 정수값으로 표현하는 Planet 열거형의 개선된 형태입니다.
enum Planet: Int {
case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}
자동 증가는 Planet.Venus가 2의 원시 값을 갖는 식으로 진행되는 것을 의미합니다.
열거형 멤버의 원시 값들을 toRaw 메소드로 읽읍시다:
let earthsOrder = Planet.Earth.toRaw()
// earthsOrder is 3
열거형의 fromRaw 메소드를 사용하여 특정한 원시 값에 해당되는 열거형 멤버를 찾읍시다. 이 예제는 원시값 7에 해당되는 행성이 Uranus임을 판별합니다:
let possiblePlanet = Planet.fromRaw(7)
// possiblePlanet is of type Planet? and equals Planet.Uranus
모든 Int
값들이 해당되는 행성을 찾을 수 있는 것은 아닙니다. 그러므로, fromRaw 메소드는 추가적인 열거형 멤버를 반환합니다. 위의 예에서, possiblePlanet 은 Planet? 타입이거나 "optional Planet" 타입입니다.
만약 9번째 위치에 있는 행성을 찾는다면, fromRaw가 반환하는 추가적 Planet 값은 nil
이 될 것입니다:
let positionToFind = 9
if let somePlanet = Planet.fromRaw(positionToFind) {
switch somePlanet {
case .Earth:
println("Mostly harmless")
default:
println("Not a safe place for humans")
}
} else {
println("There isn't a planet at position \(positionToFind)")
}
// prints "There isn't a planet at position 9"
이 예제는 9의 원시값에 해당되는 행성을 읽기 위해 추가적인 바인딩을 사용합니다.
if let somePlanet = Planet.fromRaw(9) 는 추가적인 Planet을 찾아내고, 찾을 수 있는 경우 추가적인 Planet의 내용을 somePlanet에 할당합니다. 이 경우, 9의 위치에 있는 행성을 찾는 것은 불가능하기 때문에 else
브렌치가 대신 실행됩니다.
Translator : 문대선(daeseonmoon@gmail.com)
클래스와 구조체는 프로그램의 코드블럭을 정의할때 사용됩니다. 당신의 클래스와 구조체에 기능을 더하기 위해서 상수, 변수, 그리고 함수를 정의할때와 동일한 문법으로 프로퍼티와 메서드를 정의할 수 있습니다.
다른 프로그래밍 언어와는 달리, Swift는 사용자 클래스와 구조체(custom classes and structures)를 위해서 인터페이스 파일과 구현 파일을 나누어서 만들 필요가 없습니다. Swift에서는 한 파일에서 클래스나 구조체를 정의하며, 다른 코드에서 사용하기 위한 그 클래스와 구조체의 외부 인터페이스는 자동적으로 생성됩니다.
NOTE 클래스의 인스턴스는 전통적으로 오브젝트로 알려져 있습니다. 하지만 Swifit의 클래스와 구조체는 다른 언어보다도 기능(functionality)에 가깝고, 이 챕터의 대부분은 클래스나 스트럭쳐 타입의 인스턴스에 적용 가능한 기능을 설명할 것입니다. 이런 이유때문에 일반적인 용어로서의 인스턴스가 사용될 것입니다.
Swift에서 클래스와 구조체는 여러 공통점을 가지고 있습니다. 공통적으로 가능한 것으로는:
더 많은 정보를 원하신다면 Properties, Methods, Subscripts, Initialization, Extensions 그리고 Protocols 항목을 참조하십시오.
클래스는 구조체는 할 수 없는 다음과 같은 추가적인 기능들을 지원합니다 :
더 많은 정보를 원하신다면 Inheritance, Type Casting, Initialization 그리고 Automatic Reference Counting 항목을 참조하십시오.
NOTE 여러분의 코드에서 구조체를 전달할때, 구조체는 언제나 복사가 될뿐, 참조카운팅을 사용하지 못합니다.
클래스와 구조체는 유사한 문법적 구조를 가지고 있습니다. 클래스는 class
키워드를 구조체는 struct
키워드를 사용합니다. 구조체와 클래스 모두 그들의 모든 정의는 중괄호({})내에 위치시킵니다.
class SomeClass {
// class definition goes here
}
struct SomeStructure {
// structure definition goes here
}
NOTE 새로운 클래스나 구조체를 정의할때마다 새로운 Swift의 타입을 효과적으로 정의 할 수있다.
String
,Int
, 그리고Bool
와 같은 표준의 Swift타입과 동일한 대문자 사용법과 맞도록 타입들에게SomeClass
나SomeStructure
와 같은UserCamelCase
에 따른 이름을 주십시오. 역으로 프로퍼티와 메서드는 이들과 타입이름으로 구분이 되도록frameRate
나incrementCount
와 같은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 property)를 가지고 있습니다. 저장된 프로퍼티는 클래스의 변수나 상수로서 구성되고 저장된 변수나 상수입니다. 이 두 프로퍼티는 정수값 0으로 초기화된 int
타입으로 표현됩니다.
위의 예제는 또한 비디오 화면을 위한 특정 비디오 모드를 정의하는 VideoMode
라는 클래스를 정의합니다. 이 클래스는 네개의 변수인 저장된 프로퍼티를 가지고 있습니다. 첫번째로 resolution
은 새로운 Resolution
구조체의 인스턴스로 초기화됩니다. 즉 Resolution
의 프로퍼티 타입으로 표현됩니다. 나머지 세개의 프로퍼티들은, 새로운 VideoMode
인스턴스들은 각각
interanced
는 non-interlaced 비디오라는 의미의 false
로 초기화 되고, 재생시 frame Rate는 0.0으로 초기화 된다. 그리고 name
이라 불리는 옵셔널 String
값이 있다. name
프로퍼티는 옵셔널 타입이기 때문에 자동적으로 "name
프로퍼티에 값이 없다"는 의미인 nil
로 기본값이 주어집니다.
Resolution
구조체와 VideoMode
클래스는 오직 Resolution
또는 VideoMode
가 어떻게 보일지를 정의할뿐, 특정한 해상도나 비디오 모드를 표현하지는 않습니다. 그러기에, 여러분은 구조체나 클래스의 인스턴스를 만들 필요가 있습니다.
구조체나 클래스 인스턴스를 생성하기 위한 문법은 매우 유사합니다:
let someResolution = Resolution()
let someVideoMode = VideoMode()
구조체와 클래스는 둘 다 새 인스턴스를 생성하기위해 Initializer 문법을 사용합니다. 가장 간단한 형태의 Initializer 문법은 Resolution()
이나 VideoMode()
와 같이 클래스나 구조체의 타입 이름에 빈 괄호(())를 덧붙인 것을 사용하는 것입니다. 이는 각 프로퍼티가 기본값으로 초기화 되어 있는 클래스나 구조체의 새 인스턴스를 생성합니다. 자세한 클래스와 구조체의 초기화는 Initialization 항목을 참조하십시오.
dot(.) 문법을 사용해서 인스턴스의 프로퍼티에 접근할 수 있습니다. dot 문법에서, 인스턴스 이름 뒤에 아무런 공간 없이 바로 dot(.)과 프로퍼티 네임을 적는것입니다.
println("The width of someResolution is \(someResolution.width)")
// prints "The width of someResolution is 0"
이 예제에서 someResolution.width
는 someResolution
의 width
프로퍼티를 참조하고 기본 초기값인 0를 반환합니다.
여러분은 원하는 정보를 찾기 위해 내부 프로퍼티로 계속 들어갈 수 있습니다. 예를 들면 VideoMode
에 속한 resolution
프로퍼티내의 width
프로퍼티와 같이 말입니다.
println("The width of someVideoMode is \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is 0"
dot 문법을 통해 변수 프로퍼티로서 새로운 값을 할당하는것도 가능합니다.
someVideoMode.resolution.width = 1280
println("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// prints "The width of someVideoMode is now 1280"
NOTE Objective-C와는 달리 Swift는 구조체 프로퍼티의 내부프로퍼티들을 직접적으로 설정하는 것이 가능합니다. 위의 마지막 예제를 보면,
someVideoMode
의resulotion
프로퍼티내의width
프로퍼티의 값을resolution
프로퍼티의 전체를 새로운 값으로 설정 할 필요없이 직접적으로 설정하고 있습니다.
모든 구조체는 여러분이 새로은 구조체 인스턴스의 멤버 프로퍼티들을 초기화 할수있는 자동 생성된 멤버들의 initializer(memberwise initializer) 가지고 있습니다. 새로운 인스턴스의 프로퍼티들을 위한 초기값들은 이름을 통해서 멤버들의 initializer에게 전달 될 수 있습니다.
let vga = Resolution(width: 640, height: 480)
구조체와 다르게, 클래스 인스턴스는 기본 멤버들의 initializer를 받지 않습니다. Initializer의 자세한 사항은 Initialization을 참조해주십시오.
값 타입(value type)은 변수나 상수에게 할당될 때나 함수에게 값이 전달될 때, 복사되는 타입입니다.
여러분은 지금까지 전 챕터까지 내내 값 타입을 광범위하게 사용했습니다. 사실 Swift에서 기본 형- 정수, 부동 소숫점수, 이진형, 문자열, 배열과 딕셔너리-은 전부 값형식이고 보이지 않는 곳에서 구조체로 구현되어 있습니다.
Swift에서 모든 구조체와 열거형은 값 타입입니다. 즉 여러분이 생성하는 모든 구조체와 열거형 인스턴스들, -그리고 프로퍼티로서 그들이 가지고 있는 모든 값 타입-은 여러분의 코드내에서 전달되는 경우에는 언제나 복사됩니다.
앞의 예제에서 사용된 예제에서 Resolution
구조체의 사용에 대해서 더 생각해보자:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
이 예제는 hd
라는 상수를 선언하고 full HD video(1920 픽셀 넓이에 1080 픽셀 높이)의 넓이와 높이로 초기화된 Resolution
인스턴스로 설정하였습니다.
그리고 cinema
라는 변수를 선언하고 hd
상수의 현재 값으로 설정했습니다. Resolution
이 구조체이기 때문에 존재하는 인스턴스의 복사본이 만들어지고, 이 새로운 복사본이 cinema
에 할당됩니다. hd
와 cinema
가 현재 같은 넓이와 높이 값을 가지고 있다하더라도, 그들은 보이지 않는 곳에서는 완전히 다른 두 개의 인스턴스들입니다.
다음은 cinema
의 width
프로퍼티에 디지털 시네마 프로젝션을 위해 사용되는 slightly-wider 2K 표준값의(2048 픽셀 넓이와 1080 픽셀 높이)의 넓이로 수정합니다.
cinema.width = 2048
cinema
인스턴스의 width
프로퍼티를 체크하는 것으로 이 값이 정말로 2048로 변했음을 볼 수 있습니다.
println("cinema is now \(cinema.width) pixels wide")
// prints "cinema is now 2048 pixels wide"
하지만 hd
인스턴스의 width
프로퍼티는 여전히 예전 값인 1920를 가지고 있습니다.
println("hd is still \(hd.width) pixels wide")
// prints "hd is still 1920 pixels wide"
cinema
에 hd
인스턴스를 할당할때 hd
에 저장되어있던 프로퍼티의 값들이 새로 생성된 cinema
인스턴스로 복사가 이루어졌음을 알수 있습니다. 결과를 보면 동일한 값을 가지고 있는 완전히 분리된 인스턴스임을 알수 있습니다. 두 인스턴스는 서로 다른 인스턴스이기 때문에 cinema
의 width
를 2048로 할당하더라도 hd
인스턴스에 저장되어있는 width 값에는 어떠한 영향도 미치지 않습니다.
열거형에도 동일한 법칙이 적용됩니다
enum CompassPoint {
case North, South, East, West
}
var currentDirection = CompassPoint.West
let rememberedDirection = currentDirection
currentDirection = .East
if rememberedDirection == .West {
println("The remembered direction is still .West")
}
// "The remembered direction is still .West" 출력
rememberedDirection
에 currentDirection
의 값이 할당될때 그 값의 복사본이 실제로 설정됩니다. 그러므로 currentDirection
의 값이 변경된후에도 rememberedDirection
에 저장된 원래 값에 복사본에는 어떠한 영향도 미치지 않습니다.
값 타입과 달리 참조 타입(reference type)은 함수로 전달되때나 상수나 변수에 할당될때 복사가 이루어지지 않습니다. 복사본 대신, 동일한 인스턴스의 레퍼런스(reference)가 사용됩니다.
위에서 정의한 VideoMode
클래스의 사용을 통한 예제가 있습니다:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
이 예제에서 우리는 tenEighty
라는 상수를 선언하고 새로 생성된 VideoMode
클래스의 인스턴스를 할당합니다. 비디오 모드는 전에 설정했던 1920 x 1080의 HD 해상도의 복사본을 할당했습니다. 또한 interlaced를 설정하고 "1080i"라는 이름을 주었습니다. 마지막으로 frame rate는 프레임 레이트를 초당 25.0 프레임으로 설정했습니다.
다음으로 tenEighty
를 alsoTenEighty
라는 새로운 상수에 할당하고, alsoTenEighty
의 프레임 레이트의 값을 수정하겠습니다.
let alsoTenEighty = tenEighty
asloTenEighty.frameRate = 30.0
클래스는 참조 타입이기때문에 tenEighty
와 alsoTenEighty
는 사실 동일한 VideoMode
인스턴스를 참조하고 있습니다. 실제적으로 그들은 단지 동일한 인스턴스를 참조하는 서로 다른 이름일뿐입니다.
아래의 예제코드를 통해 tenEighty
의 framerate
프로퍼티가 새로운 프레임 레이트 값인 30.0임을 확인할수 있습니다.
println("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// prints "The frameRate property of tenEighty is now 30.0"
tenEighty
와 alsoTenEighty
가 변수가 아니라 상수로 선언되었음을 주의깊게 보십시오. tenEighty
와 alsoTenEighty
상수의 그자체는 실제적으로 바뀌지 않기때문에 여러분은 여전히 tenEighty.frameRate
과 alsoTenEighty.frameRate
의 값을 바꿀수 있습니다.
tenEighty
와 alsoTenEighty
자체는 VideoMode
인스턴스를 "저장"하지 않고 보이지 않는 곳에서 VideoMode
인스턴스를 참조만 합니다. 바뀌는것은 참조되고 있는 VideoMode
의 frameRate
프로퍼티이지 VideoMode
를 참조하고 있는 상수의 값은 변하지 않습니다.
클래스는 참조타입이기때문에 여러 상수나 변수가 동일한 클래스의 인스턴스를 참조하는게 가능합니다.(구조체와 열거형은 할당되거나 함수에 매개변수로 전달될때 복사가 이루어지기때문에 동일한 인스턴스의 참조는 불가능합니다.)
이러한 이유로 두 상수나 변수가 정확하게 동일한 클래스의 인스턴스를 참조하고 있는지 알아내는것은 종종 유용하게 사용됩니다. 그러한 경우를 알아내기 위해서 Swift는 아래의 두가지 식별연산자를 제공합니다
두 상수나 변수가 동일한 인스턴스를 가리키는지 검사하기 위해 위의 두 연산자를 사용하십시오.
if tenEighty === alsoTenEighty {
println("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
// "tenEighty and alsoTenEighty refer to the same Resolution instance." 출력
"동일한(identical to)"("==="로 표현된)과 "같은(equal to)"("=="로 표현된)가 같지 않다라것에 주의하십시오.
여러분이 사용자 클래스나 구조체를 정의할때 두 인스턴스가 "같은"조건을 결정하는것은 여러분의 결정입니다.
여러분만의 "같은"과 "같지않은(not equal to)"연사자를 구현하는 과정에 대한 자세한 사항은 Equivalence Operators
를 참조하십시오.
만약 여러분이 C나 C++ 또는 Objective-C를 사용해본 경험이 있으시다면 이 언어들이 메모리주소를 참조하기 위해 포인터를 사용한다는 것을 아실겁니다. 어떤 참조형식 인스턴스를 참조하는 Swift 상수나 변수는 C에서의 포인터와 유사합니다. 하지만 이것은 메모리상의 주소를 직접적으로 가르키는 것은 아니고 또한 여러분이 생성한 인스턴스를 가르키기 위해 asterisk(*)를 필요로 하지도 않습니다. 대신 Swift에서는 이러한 레퍼런스들은 다른 상수나 변수처럼 정의할수 있습니다.
여러분 프로그램 코드의 특정 분리된 블록을 사용자 데이터 형으로 정의하기위해 여러분은 클래스나 구조체를 사용할수 있습니다.
하지만 구조체 인스턴스는 언제나 값을 전달하고 클래스 인스턴스는 참조변수를 전달합니다. 즉 이것은 이들이 서로 다른 종류의 작업에 적합하다는것을 뜻합니다. 여러분은 프로젝트에 필요한 데이터 집합이나 기능을 정의할때 그것들이 클래스로 정의되어야 할지 구조체로 정의되어야 할지 결정해야 한다는걸 생각하십시오.
일반적인 가이드로는 아래의 조건중에 한가지또는 그 이상일 경우에는 구조체를 생각하십시오.
구조체를 사용하는 좋은 예:
Double
형을 갖는 width와 height 프로퍼티의 캡슐화를 하는 기하학적 모형의 사이즈.Int
형을 갖는 start와 length 프로퍼티의 캡슐화를 하는 시리즈의 범위에 접근하는 방법.Double
형을 갖는 x,y와 z 프로퍼티의 캡슈화를 하는 3차원 좌표시스템의 포인터.이외의 경우에는 클래스로 정의하고 레퍼런스로 전달되고 관리되는 클래스의 인스턴스를 생성하십시오. 실질적으로는 대부분의 사용자 데이터 형은 구조체가 아닌 클래스로 정의되어야 합니다.
Swift의 Array
와 Dictionary
형은 구조체로 구현되어 있습니다. 하지만 배열의 경우에는 다른 구조체가 함수나 메소드에 전달될때나 상수나 변수에 할당될때와는 약간 다르게 복사가 작동합니다.
이후에 설명할 Array
와 Dictionary
의 복사는 구조체가 아닌 클래스로 구현된 NSArray
와 NSDictionary
의 복사와도 또한 다르게 작동합니다. NSArray
와 NSDictionary
인스턴스는 언제나 복사가 아니라 인스턴스의 레퍼런스가 전달되거나 할당됩니다.
NOTE 밑에 설명은 배열, 딕셔너리, 문자열 그리고 다른 값의 "복제"를 설명합니다. 복제가 언급된곳에서 여러분은 여러분의 코드가 언제나 복사처럼 작동하는것을 보게 될것입니다. 하지만 Swift는 절대적으로 필요할 경우에만 실제 값의 복사가 일어납니다. Swift는 추가적인 성능적 향상을 위해서 모든 값의 복사를 관리합니다. 그리고 이러한 최적화를 선점하기위해서 대체적인 할당문의 사용을 해서는 안됩니다.
여러분이 Dictionary
인스턴스를 상수 또는 변수에 할당할때나 함수또는 메서드에 매개변수로 전달할때 딕셔너리는 할당이되거나 함수가 호츨되는 그 시점에 복제가 됩니다. 이 과정의 자세한 사항은 Structures and Enumerations Are Value Types 항목을 참조하십시오.
만약 딕셔너리 인스턴스에 저장되어있는 키 또는 값이 값형식(구조체이거나 열거형)일 경우 그들 역시 할당될시나 함수의 호출시에 복제가 일어납니다. 이와는 다르게 만약 키 또는 값이 참조형식(클래스이거나 함수)일 경우에는 레퍼런스의 복제가 일어납니다. 하지만 이것은 그들이 참조하고 있는 클래스 인스턴스나 함수가 아닙니다. 이러한 딕셔너리의 키또는 값의 복제 방식은 구조체가 복사될때 구조체의 저장속성의 복제방식과 같습니다.
밑의 예제에서는 네 사람의 이름과 나이를 갖는 ages
라는 딕셔너리를 정의합니다. copiedAges
라 명명된 새로운 변수에 이 ages
딕셔너리를 할당합니다. 할당후에 ages
와 copiedAges
는 서로 다른 딕셔너리입니다.
var ages = ["Peter": 23, "Wei": 35, "Anish": 65, "Katya": 19]
var copiedAges = ages
이 딕셔너리의 키는 String
타입이고 값은 Int
타입입니다. 두 형은 Swift에서 값 타입입니다. 그러므로 딕셔너리의 복제가 일어날때 키와 값들 또한 복제됩니다.
여러분은 두 딕셔너리중에 하나의 age값을 바꾸고 확인함으로써 ages
딕셔너리가 복제되었음을 증명할수 있습니다. 여러분이 copiedAges
딕셔너리의 "Peter"
의 값을 24로 바꿔도 ages
딕셔너리의 반환값은 복제가 일어나기전과 동일한 23을 반환함을 알수 있습니다.
copiedAges["Peter"] = 24
println(ages["Peter"])
// "23" 출력
Swift의 배열 타입의 할당과 복제방식은 딕셔너리 타입보다 더 복잡합니다. Array
는 여러분이 배열의 요소들을 다룰때와 복제할때 반드시 필요할 경우에만 복제를 행함으로써 C와 비슷한 성능을 제공합니다. 만약 여러분이 Array
인스턴스를 상수또는 변수에 할당하거나 함수 또는 메서드의 파라미터로 전달할때 배열의 요소들은 할당이 될때나 함수가 호츨될때 복제되지 않습니다. 대신 두 배열은 동일하게 정렬된 요소들의 값을 공유합니다. 여러분은 한 배열에서 요소의 값을 수정할때 다른 또 하나의 배열을 통해서 그 결과를 관찰하실 수 있습니다.
배열에서 복제는 여러분이 배열의 길이를 수정할 가능성이 있는 코드를 실행할때 일어납니다. 이것은 요소의 추가, 삽입, 삭제 또는 배열요소들의 범위를 바꾸기 위해 사용되어지는 범위지정된 subscript들을 포함합니다. 배열의 복제가 일어날때의 배열 요소들의 복제 작동방식은 Assignment and Copy Behavior for Dictionaries에 설명된 딕셔너리의 키, 값의 복제와 동일합니다.
아래 예제는 a
라 명명된 변수에 Int
값들을 갖는 새로운 배열을 할당합니다. 그리고 이 배열은 또다시 b
와 c
로 명명된 두 변수에 할당됩니다.
var a = [1, 2, 3]
var b = a
var c = a
여러분은 supscript 문법을 통해 a 또는 b 또는 c 배열의 첫번째 값을 구할수 있습니다.
println(a[0])
// 1
println(b[0])
// 1
println(c[0])
// 1
만약 여러분이 supscript 문법을 통해 배열에 새로운 값을 할당하면 a
, b
, c
세개의 배열은 새로 할당된 값을 반환할것입니다. supcript 문법을 통한 단일 값의 수정은 배열의 길이를 변화시키지 않기때문에 배열의 요소에 새로운 값을 할당할때에는 복제가 일어나지 않습니다.
a[0] = 42
println(a[0])
// 42
println(b[0])
// 42
println(c[0])
// 42
하지만 만약 여러분이 a
배열에 새로운 요소를 추가한다면 여러분은 배열의 길이를 수정하게 됩니다. 이것은 Swift로 하여금 요소가 추가될시에 새로운 배열의 복제를 생성하게 합니다. 더욱이 a
는 별도의 독립적인 원배열의 복제된 배열입니다.
만약 여러분이 복제가 된후에 a
배열의 요소를 수정하면 a
는 여전히 원배열 요소를 참조하고 있는 b
나 c
와는 다른 값을 반환할것입니다.
a.append(4)
a[0] = 777
println(a[0])
// 777
println(b[0])
// 42
println(c[0])
// 42
배열을 함수나 메소드에 전달하거나 배열의 요소들을 조작하기전에 그 배열이 유니크한지 확인하는것은 유용합니다. 배열형 변수의 메소드인 unshare
를 호출함으로써 여러분은 배열의 유니크함을 확인하실 수 있습니다. (unshare
메소드는 상수 배열로는 호출할수 없습니다.)
만약 여러 변수들이 동일한 배열을 참조하고 있고 여러분이 그중에 하나의 변수를 이용해서 unshare
메소드를 호출했다면 그 배열은 복제가 됨으로써 그 변수가 그 변수만의 독립적인 배열의 복사를 가지게 됩니다. 하지만 그 변수가 그 배열에 대한 유일한 참조변수라면 복제가 일어나지 않습니다.
위 예제코드의 마지막에 b
와 c
는 동일한 배열을 참조하고 있습니다 b
배열 unshare
메소드를 호출해서 유니크한 배열을 만들도록 하겠습니다.
b.unshare()
만약 여러분이 unshare
메소드를 호출한뒤에 b
배열의 첫번째 요소의 값을 수정한다면 세 배열은 모두 다른 값을 보여줄겁니다.
b[0] = -105
println(a[0])
// 777
println(b[0])
// -105
println(c[0])
// 42
식별 연산자(===와 !===)를 통해 하나 이상의 배열 또는 subarray들이 동일한 저장소와 요소들을 공유하는지를 확인할수 있습니다.
아래 예제에서는 "동일한(identical to)" 연산자(===)를 사용해서 배열 b
와 c
가 여전히 배열요소들을 공유하는지 확인합니다.
if b === c {
println("b and c still share the same array elements.")
} else {
println("b and c now refer to two independent sets of array elements.")
}
// prints "b and c now refer to two independent sets of array elements."
또한 식별연산자를 사용해 subarray들이 동일한 요소를 공유하는지도 검사할수 있습니다. 아래 예제는 b
의 동일한 subarray를 비교함으로써 그 둘이 동일한 요소를 참조하고 있음을 확인합니다.
if b[0...1] === b[0...1] {
println("These two subarrays share the same elements.")
} else {
println("These two subarrays do not share the same elements.")
}
// prints "These two subarrays share the same elements."
배열의 copy
메서드를 호출함으로 강제적으로 배열의 복제를 할수 있습니다. 이 메서드는 얕은복제(shallow copy)를 행하며 복사된 요소들을 갖는 새로운 배열을 반환합니다.
아래 예제에서 우리는 names
라는 배열을 정의하고 7명의 이름을 저장합니다. copiedNames
로 명명된 새로운 변수에 names
배열의 copy
메소드를 호출하고 결과값을 할당합니다.
var names = ["Mohsen", "Hilary", "Justyn", "Amy", "Rich", "Graham", "Vic"]
var copiedNames = names.copy()
여러분은 둘중 하나의 배열 요소의 값을 수정하고 다른 배열에서의 요소값을 확인함으로써 names
배열의 복제가 제대로 이루어졌는지 확인하실수 있습니다.
만약 여러분이 copiedNames
배열의 첫번째 요소의 값을 Mohsen
에서 "Mo"
로 수정해도 names
배열은 여전히 복제가 일어나기전의 원래 값인 "Mohsen"
을 반환합니다.
copiedNames[0] = "Mo"
println(names[0])
// "Mohsen" 출력
NOTE 여러분이 단지 특정배열이 존재하는 유일한 레퍼런스임을 확실시 하시고 싶으시다면
copy
가 아닌unshare
메소드를 호출하십시오.unshare
메소드는 필요한 경우가 아닐경우 배열의 복제를 생성하지 않습니다. 반면copy
메소드는 그 배열이 다른 배열과 공유하고 있지 않더라도 언제나 복제 배열을 생성합니다.
Translator : mango (minkyu.shim@gmail.com)
속성은 특정 클래스, 구조체(structure), 혹은 열거형(Enumeration)과 값들을 연결해준다. 저장속성(Stored property)는 상수나 변수값을 인스턴스의 일부로 저장한다. 계산속성(Computed property)는 값을 그냥 저장하는 것이 아니라 계산한다. 계산속성은 클래스나 구조체 그리고 열거형에서 사용할 수 있다. 저장속성은 클래스와 구조체에서만 사용할 수 있다.
저장속성과 계산속성은 일반적으로 특정 타입의 인스턴스와 연결된다. 하지만, 속성이 타입 자체와 연결될수도 있는데, 이런 속성을 타입 속성(type property)라고 한다.
덧붙여, 프로그래머는 속성값의 변경을 모니터링하기 위해 속성 관찰/감시자(property observer)를 정의할 수 있다. 이것은 프로그래머가 속성값의 변경에 직접 정의한 동작들로 대응할 수 있게 해준다. 속성감시자는 프로그래머가 직접 정의한 저장속성이나 상위클래스에서 상속받은 속성들에 추가할 수 있다.
저장 속성은 가장 단순한 형태일때 특정 클래스와 구조체의 인스턴스에 저장되는 상수나 변수다. var
키워드로 선언(introduced)되면, 변수저장속성(variable stored property). let
키워드로 선언되면 상수저장속성(constant stored property)이라 한다.
프로그래머는 저장 속성을 정의할 때 초기값을 지정할 수 있다. 이에 대해서는 "초기속성값" 챕터에서 설명되어있다. 저장된 속성의 초기값을 수정할 수도 있는데, 심지어 상수 저장속성마저도 수정이 가능하다. 이에 대해서는 "초기화시 상수속성수정/변경하기" 챕터에 설명되어있다.
아래 예제는 FixedLengthRange
라는 구조체를 정의한다. 이 구조체는 특정 범위의 정수들을 의미하는데, 이 범위는 한번 생성되면 수정되지 않는다.
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfT hreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer val ues 6, 7, and 8
FixedLengthRange
구조체의 인스턴스들은 변수저장속성 firstValue
와 상수저장속성 length
를 갖는다. 위의 예제에서는 length
속성은 상수속성이기 때문에 구조체가 생성될때 최초로 지정되고, 이후로는 변경되지 않는다.
상수로 선언된 구조체 인스턴스의 속성들은 변수속성이더라도 수정되지 않는다.
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even thought firstValue is a variable property
위 예제에서rangeOfFourItems
인스턴스는 let
키워드를 통해 상수로 선언되었으므로 그 속성인 firstValue
는 비록 변수속성이더라도 수정할 수 없다.
이러한 동작은 구조체가 값타입(value type)이기 때문인데, 값타입의 인스턴스가 상수로 선언되면 그 속성들도 모두 상수가 된다.
이와 비교해서, 참조타입(reference type)인 클래스는 다르게 동작한다. 참조타입의 인스턴스를 상수로 선언하더라고 그 변수 속성들은 여전히 수정이 가능하다.
게으른 저장속성은 그 초기값이 최초로 사용되기전까지는 계산되지 않는다. 프로그래머는 선언시에 @lazy
attribute라고 써줌으로써 게으른 저장속성을 표시할 수 있다.
NOTE 게으론 속성은 초기화(initialization)가 끝난 뒤에도 초기값을 꺼낼 수 없을지도 모르기 때문에 언제나
var
키워드를 통해 변수로 선언되어야한다. 반면, 상수 속성은 초기화가 끝나기 전에 반드시 값을 가져야하기 때문에 게으른 속성으로 선언될 수 없다.
게으른 속성은 속성의 초기값이 객체의 초기화가 끝날때까지도 값을 알 수 없는 외부 변수에 의존하고 있을때 유용하다. 게으른 속성은 속성의 값이 매우 복잡하거나 리소스를 많이 사용하는(expensive) 계산이어서 필요한 경우가 아니면 수행되지 말아야 하는 경우에도 역시 유용하다.
아래 예시는 복잡한 클래스의 불필요한 초기화를 피하기 위해 게으른 저장속성을 사용하고 있다. 예시된 코드는 DataImporter
클래스와 DataManager
클래스 정의의 일부분이다.
cclass DataImporter {
/*
DataImporter is a class to import data from an external file.
The class is assumed to take a non-trivial amount of time to initialize.
*/
var fileName = "data.txt"
// the DataImporter class would provide data importing functionality here
}
class DataManager {
@lazy var importer = DataImporter()
var data = String[]()
// the DataManager class would provide data management functionality here
}
let manager = DataManager()
manager.data += "Some data"
manager.data += "Some more data"
// the DataImporter instance for the importer property has not yet been created
DataManager
클래스는 data
라는 저장속성을 가지는데, 이 data
저장속성은 새로운 String
값들로 이루어진 빈 배열로 초기화된다. 나머지 기능들은 코드에 드러나지 않지만, DataManager
클래스의 목적은 이 String
데이터의 배열을 외부에서 접근하여 사용하도록 관리하는 것이다.
DataManager
클래스의 기능 중 하나는 파일에서 데이터를 가져오는 것이다. 이 기능은 초기화하는데 많은 시간이 드는 DataImporter
클래스가 제공한다. 이것은 DataImporter
인스턴스가 초기화될때 화일로부터 데이터를 읽어 메모리로 로드해야하기 때문이라고 가정하자.
DataManage
r 인스턴스가 데이터를 관리할 때 파일에서 읽어오지 않는 경우도 있을 수 있다. 이런 경우엔 DataManager
가 생성될때, DataImporter
인스턴스를 생성하는 것은 불필요하다. 대신에, DataImporter
인스턴스를 최초로 사용할때 생성되도록 하는 것이 더 좋다.
게으른 속성(@lazy
)으로 표시되어있기 때문에, DataImporte
r 인스턴스인 DataManager
의 importer
속성은 fileName
속성을 조회할 때와 같은 최초의 접근시 생성된다.
println(manager.importer.fileName)
// the DataImporter instance for the importer property has now been created
// prints "data.txt"
Objective-C에 경험이 있는 프로그래머라면, 클래스 인스턴스에 값이나 참조를 저장하는 두가지 방법을 알고 있을 것이다. 속성과 별도로, 프로그래머는 인스턴스 변수를 속성에 저장된 값들의 저장소(backing store)로 활용할 수 있다.
Swift는 위의 개념들을 하나의 속성 선언에 통합시켰다. 스위프트에서 속성은 (Objective-C와 달리) 대응되는 인스턴스 변수가 없고, 속성의 저장소에도 직접 접근할 수 없다. 이러한 접근방식으로 Swift는 서로 다른 맥락에서 하나의 값이 접근되는 방식에 대한 혼란을 줄이고, 속성의 선언을 하나의 정의문에 단순화 시켰다.
저장속성에 더해서, 클래스, 구조체 그리고 열거체에는 계산속성(Computed properties)를 정의할 수 있다. 계산속성은 실제로 값을 저장하지는 않고, 다른 속성이나 값들이 간접적으로 접근하여 값을 조회하거나 수정할 수 있는 getter와 선택적인 setter를 제공한다.
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var width = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
println("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// prints "square.origin is now at (10.0, 10.0)"
위 예제는 기하학의 도형을 다루기 위한 세개의 구조체를 정의하고 있다.
Point
구조체는 x
,y
좌표를 가진다.Size
구조체는 width
(너비)와 height
(높이)를 가진다.Rect
구조체는 origin
(시작점)과 size
(크기)로 사각형을 정의한다.Rect
구조체는 center
라는 이름의 계산속성도 제공한다. Rect
구조체의 현재 중점(center position)은 언제나 origin
과 size
에 의해 결정된다. 그러므로, 프로그래머는 중점을 명시적인 Point
값으로 저장하지 않아도 된다. 대신에, Rect
구조체는 center
라는 이름의 저장속성을 위한 맞춤 getter와 setter를 제공한다. 프로그래머는 이 center
계산속성을 마치 실제 저장속성인 것처럼 사용할 수 있다.
이어지는 코드에서는 square
란 이름의 새로운 Rect
변수가 생성된다. square
변수는 시작점 (0,0)과 너비 10 ,높이 10으로 초기화된다. 이 square는 아래 도표의 파란색 사각형으로 표시된다.
square
변수의 center
속성은 마침표(.)를 통해 square.center
처럼 접근할 수 있다. 이렇게 접근할 경우, 현재 속성값을 조회하는 getter가 호출된다. 이 getter는 실재하는 속성값을 반환하는게 아니라, 계산을 통해서 현재 사각형의 중점에 해당하는 새로운 Point
값을 반환한다. 위에서 보듯이, getter는 정확하게 (5,5) 좌표를 반환한다.
그 다음엔 center
속성을 (15,15)로 변경한다. 이것은 사각형을 아래 도표 속의 우상단에 있는 오렌지 사각형의 위치로 이동시킨다. center
속성에 값을 지정하게 되면 setter를 호출하게 되는데, 이것은 x
, y
저장속성을 함께 변경시켜, 사각형이 새로운 위치로 이동하도록 만든다.
계산속성의 setter에 새로운 값이 저장될 이름이 명시되지 않으면 기본값으로 newValue
를 사용한다. 속기식의 방식을 이용한 Rect
구조체의 새로운 버전은 다음과 같다:
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
getter만 있고, setter가 없는 계산속성은 읽기전용 계산속성(read-only computed property)라 부른다. 읽기전용 계산속성은 언제나 값을 반환하며, 마침표(.)를 통해 접근할 수 있지만, 다른 값으로 설정할 수는 없다.
NOTE 읽기전용을 포함한 모든 계산속성은 반드시
var
키워드로 선언되어야한다. 왜냐하면, 계산속성의 값은 고정되지 않았기 때문이다.let
키워드는 초기화시 한번 지정되면 변경할 수 없다는 것을 표시하기 위해 상수속성 선언에만 사용해야한다.
읽기전용 계산속성의 선언은 단순히 get
키워드와 중괄호를 제거하면 된다. (역자 주 : 엄밀하게 말하자만, set 부분은 아예 없고, get 블록 내부의 코드가 한단계 바깥으로 나오는 형상)
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
println("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// prints "the volume of fourByFiveByTwo is 40.0"
위 예제코드는 Cuboid란 이름의 새로운 구조체를 정의합니다. Cuboid는 너비, 높이, 그리고 깊이를 가진 3D 직육면체 상자를 표시합니다. 이 구조체는 volume이라는 읽기전용 계산속성을 제공합니다. volume 은 현재 cuboid의 면적을 계산하여 반환합니다. 면적이 변경되면 어떤 너비, 높이, 그리고 깊이값으로 변경되어야하는지 모호하기 때문에 면적을 변경하는 것은 말이 되지 않습니다. 그럼에도 불구하고, Cuboid 구조체를 사용하는 프로그래머에게 계산된 면적으로 제공하는 읽기전용 계산속성을 제공하는 것은 유용할 것입니다.
속성 감시자는 속성값 변경를 감시하고 대응한다. 속성 감시자는 속성값이 설정될 때마다 호출되는데 새 값이 현재값과 동일하다라고 하더라도 호출된다.
지연 저장 속성은 별도로 선언한 모든 저장속성에 속성 감시자를 추가할 수 있다. 하위 클래스 안에서 속성을 오버라이딩해서 모든 상속받은 속성(저장 속성이나 계산 속성에 관계 없이)에도 속성 감시자를 추가할 수 있다. 속성 오버라이딩은 Overriding 항목에 설명한다.
NOTE 오버라이딩 되지 않은 계산 속성에 속성감시자를 추가할 필요는 없다. 계산 속성 설정자(setter)에서 직접 해당 값 변화를 감시하고 대응할 수 있기 때문이다.
두가지 중 하나나 모두 설정할 수 있는 옵션을 속성에 대한 감시자에 정의할 수 있다.
willSet
는 값이 저장되지 직전에 호출된다.didSet
는 새 값이 저장된 직후에 즉시 호출된다.만약에 willSet
관찰자를 구현한다면, 새 속성값은 상수 매개변수로 전달된다. willSet
구현의 일부분으로 이 매개변수 이름을 정의할 수 있다. 만약 매개변수 이름과 둥근괄호를 구현에 작성하지 않기로 결정해도, 여전히 기본 매개변수 이름인 newValue
로 매개변수를 사용할 수 있을 것이다.
비슷하게, didSet
감시자를 구현한다면 옛 속성값을 담고 있는 상수 매개변수를 전달할 것이다. 매개변수 이름을 원한다면 명명하고 혹은, oldValue
이라는 기본 매개변수 이름을 사용할 수 있다.
NOTE
willSet
과didSet
감시자는 속성이 최초로 초기화될 때는 호출되지 않는다. 속성값이 초기화 문맥을 벗어나서 설정되는 경우에만 호출된다.
willSet
과 didSet
실제 예제가 있다. 아래 예는 StepCounter
이라는 새 클래스를 정의하는데 이는 어떤 사람이 걷는 동안 걸은 전체 걸음 수를 추적한다. 이 클래스는 만보계나 다른 보수계로부터 입력 데이터를 받아 사용받아 일상 하루 동안 어떤 사람의 운동량을 추적할 것이다.
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
println("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
println("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps
StepCounter
클래스에는 Int
타입의 totalSteps
속성이 선언되어 있는데 willSet
과 didSet
감시자를 가지는 저장 속성이다.
totalSteps
를 위한 willSet
과 didSet
감시자는 속성에 새값이 할당될 때마다 호출된다. 심지어 현재 값과 새 값이 같을 때에도 말이다.
이 예에서 willSet
감시자는 newTotalSteps
이라는 이름의 커스텀 매개변수를 사용한다. 이 예에서는 단순히 설정될 값에 대해 출력한다.
didSet
감시자는 totalSteps
의 값이 갱신된 직후에 호출된다. totalSteps
의 새 값과 옛 값을 비교한다. 만약 총 걸음수가 증가한다면, 얼마나 많은 걸음을 걸었는지 알려줄 메시지가 출력된다. didSet
감시자는 옛값을 위한 커스텀 매개변수 이름을 제공하지 않고, 대신 기본 이름인 oldValue
를 사용한다.
NOTE 만약
didSet
감시자 자체에서 속성을 할당한다면, 새로 할당한 값은 좀 전에 막 설정되었던 값을 대신할 것이다.
이전에 설명한 계산 속성과 관찰 속성에 대한 능력은 전역 변수와 지역변수에서도 가능하다. 전역 변수가 모든 함수, 메소드, 클로저, 타입의 문맥 밖에 정의된 변수라면, 지역 변수는 어떤 함수, 메소드, 클로저 문맥 안에 정의된 변수이다.
이전장에서 만난 전역 변수와 지역 변수는 모두 저장 변수 뿐이었다. 저장 변수란 저장 속성처럼 특정 타입의 값에 대한 저장소를 제공하고 그 값을 설정하거나 집어올 수 있도록 해준다.
그러나, 전역이냐 지역 유효범위이냐 관계 없이 계산변수나 저장 변수를 위한 관찰자를 또한 정의할 수 있다. 계산 변수는 값을 저장한다기 보다 계산을 하고 계산속성에서와 동일한 방식으로 작성하면 된다.
NOTE 전역 상수와 변수는 Lazy Stored Properties와 유사한 방식으로 항상 지연 계산한다. 그러나 지연 저장 속성과는 다르게 전역 상수와 변수는
@lazy
이라는 특성으로 표시하지 않아도 된다.
인스턴스 속성은 특정 타입의 인스턴스에 속한 속성이다. 해당 타입에 대한 새 인스턴스를 생성할 때마다 다른 인스턴스와 분리된 인스턴스 자신의 속성값 세트를 가지게 된다.
타입의 어떤 하나의 인스턴스에 속한 것이 아닌 해당 타입 자체에 속한 속성 역시 정의할 수 있다. 이 속성에 대해서는 얼마나 많은 인스턴스를 만들었는지에 관계없이 단 한개의 복사본만이 존재할 것이다. 이런 종류의 속성을 타입 속성이라고 한다.
타입속성은 특정 타입의 모든인스턴스에 영향을 미치는 값을 정의하는데 유용하다. 모든 인스턴스가 사용하는 상수 속성이라든지 (C의 정적 상수 같이), 특정 타입의 모든 인스턴스가 글로벌하게 값을 저장하는 변수 속성(C의 정적 변수 같이) 같은 경우가 있다.
값타입에서는 (즉, 구조체와 열거형), 저장 과 계산 타입 속성을 정의할 수 있다. 클래스에서는 계산 타입 속성만을 정의할 수 있다.
값 타입에서 저장 타입 속성은 변수가 상수가 될 수 있다. 저장 타입 속성은 항상 변수 속성으로 정의될 것이다. 저장 인스턴스 속성과 동일한 방식이다.
NOTE 위에 나온 계산 타입 속성은 읽기 전용 계산 타입 속성이지만, 계산 인스턴스 속성과 동일한 문법을 사용해서 읽고쓰기용 계산 타입 속성 또한 정의할 수 있다.
C나 Objective-C에서는 전역적인 static 변수의 타입을 가지는 static 상수와 변수를 정의할 수있다. 그러나 Swift에서는 타입 속성은 타입들의 바깥쪽 중괄호 안에 타입의 정의 부분을 적을 수 있다. 그리고 각 타입 속성은 그 타입이 지원하는 명시적인 scope를 가진다.
static
키워드를 가지고 값 타입의 타입 속성을 정의할 수 있고, class
키워드를 이용해서 클래스 타입의 타입 속성을 정의할 수 있다. 아래의 예시는 저장되거나 혹은 계산된 타입 속성을 위한 문법들을 보여준다:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
// return an Int value here
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
// return an Int value here
}
}
class SomeClass {
class var computedTypeProperty: Int {
// return an Int value here
}
}
NOTE 위의 계산된 타입 속성예지들은 읽기 전용의 계산된 타입 속성들을 위한 것이다. 그러나 당신은 똑같은 문법을 계산된 인스턴스 속성에 대하여 사용하는 것으로 읽고 쓸수 있는 계산된 타입 속성을 정의할 수 있다.
타입 속성은 인스턴스 속성처럼 닷 표기법을 이용해서 조회하고 설정한다. 그러나, 타입 속성이 타입의 인스턴스가 아니라 타입에 조회하고 설정한다. 예를들면:
println(SomeClass.computedTypeProperty)
// prints "42"
println(SomeStructure.storedTypeProperty)
// prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
println(SomeStructure.storedTypeProperty)
// prints "Another value."
이어지는 예제에서 많은 오디오 채널에 대한 오디오 레벨 미터를 추상화한 구조체 일부로 두개의 저장 타입 속성을 사용한다. 각 채널은 0
부터 10
까지를 포함한 정수형 오디오 레벨 미터를 가진다.
밑의 그림은 어떻게 이 두가지 오디오 채널이 스테레오 오디오 레벨 미터로 조합해 만드는지 그림으로 설명한다. 채널의 오디오 레벨이 0
일 때 채널의 모든 불 빛은 꺼지게 된다. 채널 오디오 레벨이 10
일 때 채널의 모든 불빛은 켜지게 된다. 그림처럼, 왼쪽 채널은 현재 9
레벨이고, 오른쪽 채널은 현재 7
레벨이다.
위에 표시된 오디오 채널은 AudioChannel
구조체의 인스턴스로 표현된다.
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// cap the new audio level to the threshold level
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// store this as the new overall maximum input level
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
AudioChannel
구조체는 기능성을 지원하기 위한 두개의 저장 타입 속성을 정의한다. 먼저, thresholdLevel
는 오디오 레벨이 받을 수 있는 최대 임계값을 정의한다. 이는 모든 AudioChannel
인스턴스를 위한 10
이라는 상수 값이다. 만약 오디오 신호가 10
보다 높은 값이 들어온다면, 이 임계값에 따라 ( 아래 설명하는 것처럼 ) 상한선이 정해질 것이다.
둘째 타입 속성은 maxInputLevelForAllChannels
이란 변수 저장 속성이다. 이는 어떤 AudioChannel
인스턴스로 받는 최대 입력값을 계속 추적한다. 그 초기값은 0부터 시작한다.
AudioChannel
구조체 역시 currentLevel
이란 저장 인스턴스 속성 정의하는데 0부터 10까지 현재 체널의 오디오 레벨를 나타낸다.
currentLevel
속성은 didSet
속성 관찰자를 가지는데 언제 설정되던간에 currentLevel
값을 확인하기 위함이다. 이 관찰자는 두가지 확인 사항을 실행한다:
currentLevel
의 새 값이 thresholdLevel
에 의해 허용된 것보다 크다면, 속성 관찰자는 currentLevel
를 thresholdLevel
으로 상한선을 맞출 것이다.currentLevel
의 새 값이(상한선이 맞춰진 다음) 이전에 아무 AudioChannel
인스턴스로부터 전달 받은 어떤 값보다도 크다면 속성관찰자는 maxInputLevelForAllChannels
정적 속성에 새 currentLevel
값을 저장할 것이다.NOTE 두가지 확인 사항 중 전자에서
didSet
관찰자는currentLevel
에 다른 값을 저장한다. 그렇지 않으면 또다시 관찰자가 호출되기 때문이다.
스테레오 사운드 시스템의 오디오 레벨을 나타내기 위해 leftChannel
, rightChannel
이란 2개의 신규 오디오 채널을 만들기 위해 AudioChannel
구조체를 사용할 수 있다.
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
만약 왼쪽 채널의 currentLevel
를 7로 설정한다면 maxInputLevelForAllChannels
타입 속성이 7
과 동일하게 갱신되는 것을 볼 수 있다.
leftChannel.currentLevel = 7
println(leftChannel.currentLevel)
// prints "7"
println(AudioChannel.maxInputLevelForAllChannels)
// prints "7"
만약 오른쪽 채널의 currentLevel
를 11로 설정하려고 하면, 오른쪽 채널의 currentLevel
속성 은 10이라는 최대값에 상한선이 맞춰지는 것을 볼 수 있고maxInputLevelForAllChannels
이란 타입 속성은 10이라는 상한선이 맞춰진다.
rightChannel.currentLevel = 11
println(rightChannel.currentLevel)
// prints "10"
println(AudioChannel.maxInputLevelForAllChannels)
// prints "10"
Translator : 북극산펭귄 (say8425@gmail.com) Translator : 허혁 (hyukhur@gmail.com)
메서드Method는 타입type에 의존적인 함수입니다. 모든 클래스와 구조체 그리고 이너멀레이션Enumeration은, 타입이 정해진 인스턴스Instance가 수행하는 작업을 캡슐화하는 인스턴스 메소드를 정의 할 수 있습니다. 또한 타입 자체에 관여하는 타입 메소드를 정의 할 수 있습니다. 이 타입 메소드는 오브젝티브-C에서의 클래스Class Method와 유사합니다.
인스턴스 메소드Instance Method는 특정 클래스, 구조체 혹은 이너멀레이션의 인스턴스에 속하는 함수입니다. 이것은 인스턴스 속성에 접근하고 수정하는 방법이나, 인스턴스의 용도에 관련된 기능을 지원합니다. 함수섹션
에서 설명된대로 인스턴스 메소드는 특히 함수와 동일한 문법을 가집니다.
여러분은 인스턴스 메소드를 해당 타입이 속한 괄호내에서 작성합니다. 인스턴스 메소드는 다른 인스턴스 메소드와 해당 타입의 속성에 대한 암시적 권한Implict access을 가지고 있습니다. 인스턴스 메소드는 오직 해당 타입이 속한 특정한 인스턴스에 의해서만 호출 될 수 있습니다. 이것은 속해있는 인스턴스 없이 독립적으로 호출 될 수 없습니다.
여기 작업을 수행한 횟수를 세는, 카운터Counter
클래스를 정의한 간단한 예제가 있습니다:
class Counter {
var count = 0
func increment() {
count++
}
func incrementBy(amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
이 Counter
클래스는 세가지 인스턴스 메소드를 정의합니다.
increment
1만큼 counter를 증가시킵니다.incrementBy(amount: Int)
특정한 정수값만큼 counter를 증가시킵니다.reset
counter를 0으로 재설정합니다.또한 Counter
클래스는 현재 카운터 값을 추적하기 위해 변수 프로퍼티Property count를 선언하였습니다.
당신은 프로퍼티와 같은 점 문법으로 인스턴스 메소드를 호출합니다:
let counter = Counter()
// 초기 counter값은 0입니다
counter.increment()
// counter값은 이제 1입니다
counter.incrementBy(5)
// counter값은 이제 6입니다
counter.reset()
// counter값은 이제 0입니다
함수 매개 변수는 외부 변수 이름 섹션에서 설명한대로 지역 이름(함수 바디에서 사용될)과 외부 이름(함수가 호출될 때 사용될)을 가질 수 있습니다. 메소드 매개 변수 또한 그렇습니다. 매소드는 그저 타입에 의존적인 함수와 마찬가지이기 때문입니다. 그러나 이 둘의 지역 이름과 외부 이름의 작명법은 다름니다.
스위프트의 메소드는 오브젝티브-C에서 사용하던 것과 매우 유사합니다. 오브젝티브-C에서 그러하였듯이, 스위프트에서 메소드 이름은, 앞서살펴본 Counter클래스 예제의 incrementBy 메소드와 같이, 형식적으로 첫번째 파라미터parameter가 with, for 또는 by와 같은 전치사를 사용합니다. 이 전치사가 사용되었다는 점은 메소드가 호출될 때 문장처럼 읽히는 것을 가능하게 만듭니다. 스위프트는 이 관습적으로 인정받은 작명법을 매서드 파라미터에 다른 기본 접근법을 사용하여 함수 파라미터보다 작성하기 쉽게 만들었습니다.
구체적으로, 스위프트는 메소드내 첫 번째 파라미터 이름은 기본적으로 지역 파라미터 이름으로 지정합니다, 그리고 두 번째 파라미터부터는 지역 파라미터와 외부 파라미터 둘다 지정합니다. 이 관습은 오브젝티브-C 메소드에서 형식적으로 이름을 짓고, 작성하던 것과 유사합니다. 그리고 파라미터 이름에 자격을 부여할 필요없이 알아보기 쉬운 메소드 호출을 만들 수 있습니다.
IncrementBy
메소드가 좀 더 복잡하게 정의된 또 다른 버전의 이 Counter
클래스를 보십시오:
class Counter {
var count: Int = 0
func incrementBy(amount: Int, numberOfTimes: Int) {
count += amount * numberOfTimes
}
}
이 incrementBy
메소드는 -amount
와 numberofTimes
라는 두가지 파라미터를 가지고 있습니다. 기본적으로 스위프트는 amount
를 지역 이름으로만 취급합니다, 하지만 numberofTimes
는 지역 이름과 외부 이름 두가지 모두로서 취급합니다. 다음 예제와 같이 호출 할 수 있습니다:
let counter = Counter()
Counter.incrementBy(5, numberOfTimes: 3)
// Counter value is now 15
당신은 첫번째 인수값에 대해 외부 파라미터 이름을 정의 해줄 필요가 없습니다, 왜냐면 incrementBy
라는 함수 이름에서 그것의 용도는 명확해졌기 때문입니다. 하지만 두번째 인수는 메소드가 호출 되었을 때, 그 용도를 명확히하기 위해서 외부 파라미터 이름으로 규정됩니다.
이 기본적인 동작은 numberOfTimes
파라미터 앞에 해쉬 심볼(#)을 붙임으로서 좀 더 효율적으로 취급할 수 있습니다:
Func incrementBy(amount: int, #numberOfTimes: int) {
Count += amount * numberOfTimes
}
위의 기본적인 동작은 스위프트의 메소드 정의는 오브젝티브-C의 문법 스타일과 유사하게 쓰인다는 것을 의미하며, 자연스럽게 호출되고 표현적으로 풍부하게 표현된다는 것을 의미합니다.
기본적인 방법은 아니지만, 가끔씩 메소드의 첫번째 파라미터에 외부 파라미터 이름을 제공하는 것이 유용 할 수 있습니다. 당신은 직접 명시적으로 외부 이름을 추가 할 수 있으며, 혹은 해쉬 심볼을 첫번째 파라미터의 이름 앞에서 붙여서 지역 이름을 외부 이름과 같이 사용할 수 있습니다.
반대로, 메소드의 두번째 파라미터나 추가 파라미터에 대해 외부 이름을 제공하고 싶지 않으면, 언더바(_)로 사용해 해당 파라미터를 명시적 외부 파라미터이름으로 오버라이드override해줄 수 있습니다.
self
프로퍼티모든 인스턴스 타입은 인스턴스 자체와 명확하게 동일한, 셀프라고 불리는 명시적 프로퍼티를 가지고 있습니다. 이 명시적 셀프 프로퍼티는, 자신이 속한 인스턴스 메소드내에서 현재 인스턴스를 참조하는 데 사용 할 수 있습니다.
다음 예제의 increment
메소드는 그 와같이 작성되었습니다.
func increment() {
self.count++;
}
실제로는 여러분이 코드에서 self
를 작성해줄 필요가 별로 없습니다. 여러분이 명시적으로 self
를 작성하지 않았다면, 스위프트는 여러분이 메소드내에서 알려진 프로퍼티나 메소드를 사용할 때마다, 현재 인스턴스의 프로퍼티나 메소드를 참조할 것을 상정하고 있습니다.
이 가정은 Counter
의 세가지 인스턴스 메소드 내부에서 (‘self.count’대신)count
를 사용함으로서 실증되었습니다.
인스턴스 메소드의 파라미터 이름이 해당 인스턴트의 속성과 동일한 이름을 가진 경우, 이 규칙의 주요 예외가 발생합니다. 이렇게 된다면, 파라미터 이름은 우선적으로 좀 더 확실하게 속성을 참조할 필요가 있습니다. 여러분은 self
속성을 확실하게 사용해서 파라미터 이름과 프로퍼티 이름을 구분지을 수 있습니다.
여기 self
는 x라고 불리는 메소드 파라미터와 역시 x라고 불리는 인스턴스 파라미터 사이에서 명확하게 구분지어주고 있습니다.
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOfX(x: Double) -> Bool {
return self.x -> x
}
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOfX(1.0) {
println(“This point to the right of the line where x == 1.0”)
}
// prints “This point is to the right of the line where x == 1.0”
self
접두사가 없다면, 아마도 스위프트는 두 x
모두 메소드 파라미터 x
를 참조한다고 여깁니다.
구조체와 열거형는 값타입이다. 기본적으로 값타입의 프로퍼티는 인스턴스 메소드 안에서 변경될 수 없다.
그러나 만약 특정 메소드 안에서 구고체나 열거형을 변경할 필요가 있다면, 그 메소드에 변화(mutating
)동작을 선택할 수 있다. 그러면 메소드는 자신 안에서 해당 프로터리를 변화(즉, 변경)시킬 수 있고, 적용된 모든 변경은 메소드가 끝나면 원본 구조체에 쓰여지게 된다. 메소드는 내포된 self
프로퍼티에 완전히 새로운 인스턴스를 할당할 수도 있다.
어떤 메소드 func
키워드 앞에 mutating
키워드를 둬서 이 동작을 선택할 수 있다.
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
x += deltaX
y += deltaY
}
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveByX(2.0, y: 3.0)
println("The point is now at (\(somePoint.x), \(somePoint.y))")
// prints "The point is now at (3.0, 4.0)
상단의 Point
구조체는 moveByX
변화할 메소드로 정의했는데, 이는 Point
인스턴스를 특정 크기만큼 옮긴다. 그런 프로퍼티를 변경시킬 수 있게 만들기 위해서 mutating
키워드를 그 정의에 추가했다.
구조체 타입의 상수에 변화메소드를 호출할 수 없다는 것을 유의해라. 왜냐하면 비록 해당 프로퍼티가 변수형태로 되어 있어도 그 프로퍼티는 변경될 수 없기 때문이다. 상수 구조체 인스턴스의 저장속성에 설정되어 있다.
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveByX(2.0, y: 3.0)
// this will report an error
Mutating
) 메소드안에서 self
에 할당하기변하는(Mutating
) 메소드는 암시적으로 self
프로퍼티에 완전한 새 인스턴스를 할당할 수도 있다. 위 예제에서 보여준 Point
는 아래와 같이 재 작성해볼 수 있다.
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(deltaX: Double, y deltaY: Double) {
self = Point(x: x + deltaX, y: y + deltaY)
}
}
이 버전의 변하는 moveByX
메소드는 x
값과 y
값을 받아 대상 위치에 설정에 완전히 새로운 구조체를 만든다. 대안 버전의 메소드 호출 최종 결과는 이전 버전 호출에서와 정확하게 동일할 것이다.
열거형에서 변하는 메소드는 동일 열거형에서 다른 구성원이 될 수 있게 암시적 self 매개변수를 설정할 수 있다.
enum TriStateSwitch {
case Off, Low, High
mutating func next() {
switch self {
case Off:
self = Low
case Low:
self = High
case High:
self = Off
}
}
}
var ovenLight = TriStateSwitch.Low
ovenLight.next()
// ovenLight is now equal to .High
ovenLight.next()
// ovenLight is now equal to .Off
이 예제에서 열거형은 3가지 상태 전이가 정의되어 있다. 3가지 전력 상태 (끔 Off
, 낮음 Low
, 높음 High
) 사이의 전이 주기는 next
메소드가 호출되는 매회이다.
위에서 설명한데로, 인스턴스 메소드는 특정타입의 인스턴스에서 호출되는 메소드이다. 타입 자체에서 호출하는 메소드 또한 정의할 수 있다. 이런 종류의 메소드를 타입 메소드라고 한다. 클래스를 위한 타입 메소드는 func
키워드 앞에 class
키워드를 써서 그리고 구조체와 열거형을 위한 타입 메소드는 func
키워드 앞에 static
키워드를 써서 지칭할 수 있다.
노트 오브젝티브씨에서는 오브젝티브씨 클래스를 위한 타입 단계 메소드만을 정의할 수 있었다. 스위프트에서는 모든 클래스, 구조체, 열거형에 타입 단계 메소드를 정의할 수 있다. 개별 타입 메소드는 지원하는 타입에 대해 명시적으로 범위를 지정한다.
타입 메소드는 인스턴스 메소드처럼 점표기법(dot syntax)으로 호출한다. 그러나 타입에 대한 타입 메소드를 호출해야지 그 타입에 대한 인스턴스를 호출하는 것이 아니다. 여기에 어떻게 SomeClass
라는 클래스에 타입 메소드를 호출하는지가 있다.
class SomeClass {
class func someTypeMethod() {
// type method implementation goes here
}
}
SomeClass.someTypeMethod()
타입 메소드 본체 안에서는 암시적 self
프로퍼티는 타입에 대한 인스턴스가 아니라 타입 그 자체를 가르킨다. 구조체와 열거형에서는 마치 인스턴스 프로퍼티와 인스턴스 메소드 매개변수에서 그랬던 것처럼 static
프로퍼티와 static
메소드 매개변수 사이의 명확하게 하기 위해 self
를 쓸 수 있다라는 것을 뜻한다.
좀 더 일반적으로, 어떤 타입 메소드의 본체 안에서 사용하는 제한없는 메소드와 프로퍼티는 다른 타입단계 메소드와 프로퍼티를 참조 할 것이다. 타입 메소드는 어떤 다른 타입 메소드의 이름과 함께 또다른 타입 메소드를 호출할 수 있다. 비슷하게, 구조체와 열거형 타입 메소드는 타입 이름 접두사 없이 정적 프로퍼티 이름을 사용해서 정적 프로퍼티에 접근할 수 이다.
아래 예제는 여러 레벨이나 게임 단계를 통해 플레이어의 진척도를 추적하는 LevelTracker
이란 구조체를 정의한다. 어떤 레벨을 끝낼때마다 그 레벨은 장비에 있는 모든 플레이어를 풀 수 있다. LevelTracker
구조체는 정적 프로퍼티와 메소드를 사용해 게임이 풀리는 레벨에 도달했는지 여부를 추적한다. 또한 개별 플레이어의 현재 수준에 대해 또한 추적한다.
struct LevelTracker {
static var highestUnlockedLevel = 1
static func unlockLevel(level: Int) {
if level > highestUnlockedLevel { highestUnlockedLevel = level }
}
static func levelIsUnlocked(level: Int) -> Bool {
return level <= highestUnlockedLevel
}
var currentLevel = 1
mutating func advanceToLevel(level: Int) -> Bool {
if LevelTracker.levelIsUnlocked(level) {
currentLevel = level
return true
} else {
return false
}
}
}
LevelTracker
구조체는 어떤 플레이어가 락을 풀고 도달한 가장 높은 레벨를 추적한다. 이 값은 highestUnlockedLevel
이라고 불리는 정적 프로퍼티이다.
LevelTracker
는 또한 highestUnlockedLevel
프로퍼티와 동작하는 두가지 타입 함수를 정의한다. 첫번째는 unlockLevel
이란 타입 함수인데 언제 새 레벨이 풀리는지에 상관없이 highestUnlockedLevel
값을 갱신한다. 두번째는 levelIsUnlocked
이라는 편리한 타입 함수로 만약 특정 레벨이 이미 풀렸으면 true
를 반환한다. (이 타입 함수들이 LevelTracker.highestUnlockedLevel
이라고 쓸 필요 없이 highestUnlockedLevel
정적 프로퍼티에 접근할 수 있다는 것을 알아둬라.
정적 프로퍼티와 타입 메소드에 추가로 LevelTracker
은 개별 플레이어의 게임 전반에 걸친 진행 상태를 추적한다. 플레이어가 현재 진행하고 있는 레벨을 추적하기 위해 currentLevel
이라는 인스턴스 프로퍼티를 사용한다.
currentLevel
프로퍼티를 관리하는데 도움이 되고자, LevelTracker
은 advanceToLevel
이라는 인스턴스 메소드를 정의했다. currentLevel
를 업데이트 하기 전에 이 메소드는 요청 받은 새 레벨이 이미 풀렸는지 아닌지를 확인한다. advanceToLevel
메소드는 실제로 currentLevel
에 설정할 수 있는지 아닌지를 알려주기 위해 Boolean
값을 반환한다.
LevelTracker
구조체는 Player
클래스와 사용하는데, 아래 보여지는 것처럼, 개별 플레이어의 진행 상태를 추적하고 갱신한다.
class Player {
var tracker = LevelTracker()
let playerName: String
func completedLevel(level: Int) {
LevelTracker.unlockLevel(level + 1)
tracker.advanceToLevel(level + 1)
}
init(name: String) {
playerName = name
}
}
Player
클래스는 플레이어의 진행상태를 추적하기 위해 LevelTracker
의 새 인스턴스를 생성한다. 또한 completedLevel
이란 메소드를 제공하는데 언제 플레이어가 특정 레벨을 완료했을 때 호출한다. 이 메소드는 모든 플레이어의 다음 레벨을 풀고 플레이어을 다음 레벨로 이동시키기 위해 진행 상태를 갱신한다. ( advanceToLevel
의 Boolean
반환값은 무시되는데 왜냐하면 레벨이란 이전줄의 LevelTracker.unlockLevel
을 호출해서 풀렸는지를 알기 위함이기 때문이다.)
신규 플레이어를 위해 Player
클래스의 인스턴스를 만들어서 플레이어가 레벨 1을 달성 했을때 어떤일이 벌어지는지 보여주겠다.
var player = Player(name: "Argyrios")
player.completedLevel(1)
println("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// prints "highest unlocked level is now 2"
만약 게임에서 어떤 플레이어도 아직 풀지 못한 레벨로 옮기려는 두번째 플레이어를 생성한다면, 플레이어의 현재 레벨을 설정하려는 시도는 실패할 것이다.
player = Player(name: "Beto")
if player.tracker.advanceToLevel(6) {
println("player is now on level 6")
} else {
println("level 6 has not yet been unlocked")
}
// prints "level 6 has not yet been unlocked"
Translator : Snowcat8436 (snowcat8436@gmail.com)
클래스, 구조체 그리고 열거형은 collection, list, sequence의 member element에 접근하기 위한 축약형인 서브스크립트로 정의할 수 있습니다. 또한 값의 설정이나 검색을 위한 별도의 메서드(seperate method)없이 index를 통해서 값을 설정하거나 검색하기 위해 서브스크립트를 사용할 수있습니다. 예를 들어서 someArray[index]
와 같이 배열
의 내부값(element)에 접근하거나 someDictionary[key]
와 같이 사용하여 딕셔너리
의 내부값(element)에 접근할 수 있습니다.
한 타입을 위해서 여러개의 서브스크립트를 정의 할 수도 있고, 또한 서브스크립트로 넘기는 index값의 타입을 기초로하여 사용하기 적절한 서브스크립트 overload(중복)를 선택할수 있다. 서브스크립트는 당신이 원하는 타입에 맞게 여러개의 입력 파라미터(input parameter)를 가지도록 정의할 수도 있다.
서브스크립트는 인스턴스의 이름 뒤에 있는 '[]'안에 한개 이상의 값을 적는 것으로 당신이 인스턴스들의 타입(instances of a type)를 요구할 수 있다. 그들의 문법은 인스턴스의 메서드나 computed property의 문법과 유사합니다. 인스턴스의 메서드들과 동일한 방식으로 subscript
키워드와 함께 특정한 하나 이상의 입력 파라미터와 와 리턴타입을 통해서 서브스크립트를 정의할 수 있다. 다만 인스턴스의 메서드들과는 달리 서브스크립트는 읽고 쓰는 권한만 있거나 읽는 권한만을 가질 수 있다. 다음 코드는 서브스크립트가 computed property들과 동일한 방식으로 getter와 setter를 통해 작업하는 것을 보여줍니다
subscript(index: Int) -> Int {
get {
// return an appropriate subscript value here
}
set(newValue) {
// perform a suitable setting action here
}
}
newValue
의 type은 해당 subscript의 리턴값과 동일합니다.
computed properties와 같이 당신은 setter의 파라미터인 (newValue)
를 특정하게 선택할수 없습니다. 만일 당신이 setter를 위한 타입을 아무것도 제공하지 않는다면, 그제서야 기본 parameter인 newValue
가 setter를 위해 제공될 것입니다.
읽기 전용의 computed properties와 같이 get
키워드를 없애서 읽기 전용의 서브스크립트를 만들 수 있다.:
subscript(index: Int) -> Int {
// return an appropriate subscript value here
}
이곳에 정수를 n배 한 결과를 표시하는 TimesTable structure
를 선언하기 을 위한 읽기 전용의 서브스크립트를 구현하는 예제가 하나 있습니다.
struct TimesTable {
let multiplier: Int
subscript(index: Int) -> Int {
return multiplier * index
}
}
let threeTimesTable = TimesTable(multiplier: 3)
println("six times three is \(threeTimesTable[6])")
// prints "six times three is 18"
이 예제에서 새로운 TimesTable
의 인스턴스는 3의 배수를 출력을 하도록 생성되고.
이것은 넘겨준 값인 3을 구조체의 initializer
가 인스턴스의 multiplier
파라미터로 사용한 것을 의미합니다.
해당 서브스크립트를 부르는 것으로 threeTimesTable
의 인스턴스에게 요청할 수 있다. 보는 바와같이 threeTimesTable[6]
과 같이 부르는 것으로 threeTimesTable
인스턴스의 서브스크립트를 부를 수 있다. 해당 요청은 6의 3배 테이블을 요청했으며 그 값은 18=6*3 이 된다.
NOTE n배 테이블은 고정된 숫자값을 출력하는 규칙에 기반합니다. 따라서 newValue등을 통하여 treeTimesTable[someindex]를 따로 설정하는 것은 적절하지 않으며 그러기에 위의 TimesTable을 위한 서브스크립트는 읽기전용의 서브스크립트로 선언되었습니다.
아주 정확한 의미의 "subscript"는 그것이 사용되는 문맥에 따라 결정된다. 서브스크립트는 일반적으로 collection, list, 또는 sequence에 특정 member elements에 접근하기 위한 단축형이라는 의미로 사용되며, 당신은 특별한 클래스나 구조체의 기능을 위해 적절한 방식으로 자유롭게 대부분의 서브스크립트를 구현할 수있다.
예를 들어서 Swift의 Dictionary
타입은 Dictionary
인스턴스에 저장된 값들을 설정하고 검색하기 위한 하나의 서브스크립트로 구현했다.
당신은 딕셔너리 안에 딕셔너리의 키의 타입의 키값을 서브스크립트의 '[]'안에 넣는 것으로 값을 세팅할 수 있으며 딕셔너리 안에 들어갈 값을 서브스크립트에 할당할 수도 있다:
var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
numberOfLegs["bird"] = 2
위 예제는 numberOfLegs
라는 변수를 선언하고 이를 3가지 key-value 쌍을 가진 딕셔너리 literal로 초기화 하고있다.
numberOfLegs
딕셔너리의 타입은 Dictionary<String, Int>
를 뜻하며. 딕셔너리가 생성이 된 후, 이 예제는 서브스크립트 assignment을 사용하여 String
키인 "bird"
와 Int
값인 2를 딕셔너리에 추가하는 것을 볼 수있다.
딕셔너리 서브스크립트에 관한 보다 많은 정보를 원한다면, Accessing and Modifying a Dicionary를 참고하기 바란다.
NOTE Swift의 딕셔너리 타입은 내부적인 key-value 서브스크립트가 요구하고 반환하는 타입이 옵셔널 타입인 서브스크립트로 구현하였습니다. 위에서 numberOfLegs 딕셔너리를 보면, key-value 서브스크립트를 요구하고 반환하는 타입이
Int
일까요?, 그렇지 않으면 옵셔널Int
일까요? 딕셔너리 타입은 '모든 키가 값을 가지는 것은 아니다'라는 사실을 위한 모델을 지원하기 위해 옵셔널 서브스크립트 타입을 사용합니다. 그리고 어떤 키에 값을 삭제하는 방법을 제공하는데, 이 경우 해당 키값의 값는nil
로 할당된다.
서브스크립트는 어떠한 숫자의 입력 파라미터들도 처리가 가능하다.그리고 그리고 이 입력 파라미터들은 어떠한 타입도 가능하다. 서브스크립트는 또한 어떠한 타입으로도 리턴이 가능하다. 서브스크립트는 변수 파라미터와 variadic parameters도 가능하지만, in-out parameters 나 default parameter 값은 지원하지 않습니다.
클래스나 구조체는 필요한 만큼의 서브스크립트를 구현하는 것이 가능하며, 적절한 서브스크립트는 보통 각각의 서브스크립트가 사용되는 요소요소에서 서브스크립트에 포함되어 서로 대비하도록 한 값이나 값들의 타입이 기초라고 생각할 수 있습니다.
이러한 다수의 서브스크립트에 관한 정의는 서브스크립트 overloading으로도 알려져 있습니다.
대부분의 한개의 파라미터만을 요구하는 서브스크립트와는 다르게, 만일 당신이 만들 것에 필요하다면, 다수의 파라미터를 요구하는 서브스크립트를 선언할 수도 있습니다.
다음 예제는 Double
값을 가지는 2차원 행렬을 표현하는 Matrix
라는 구조체를 선언하고 있습니다. Matrix
구조체의 서브스크립트는 두개의 정수형 파라미터를 요구 하고 있습니다:
struct Matrix {
let rows: Int, columns: Int
var grid: Double[]
init(rows: Int, columns: Int) {
self.rows = rows
self.columns = columns
grid = Array(count: rows * columns, repeatedValue: 0.0)
}
func indexIsValidForRow(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
subscript(row: Int, column: Int) -> Double {
get {
assert(indexIsValidForRow(row, column: column), "Index out of range")
return grid[(row * columns) + column]
}
set {
assert(indexIsValidForRow(row, column: column), "Index out of range")
grid[(row * columns) + column] = newValue
}
Matrix
는 rows
와 columns
이라는 두개의 파라미터를 요구하는 initializer 를 제공하며, Double
타입으로 rows * columns
를 충분히 저장할 수 있을만큼 큰 배열을 생성합니다. 각 Matrix
의 위치의 초기값은 0.0
으로 주어지며. 이러한 작업이 모두 이루어 진 다음에는 만들어진 배열을 배열의 initializer로 보내서 올바른 크기의 배열을 만듭니다. 이 initializer에 다한 자세한 사항은 Creating and Initializing an Array를 참고하세요.
이제 다음과 같은 방식으로 적절한 row와 column을 initializer에 넘기는 것으로 새로운 Matrix
인스턴스를 생성할 수 있습니다.
var matrix = Matrix(rows: 2, columns: 2)
아래의 예제는 2x2의 크기를 가진 새로운 Matrix
인스턴스를 생성하는 예제입니다. Matrix
인스턴스를 효과적이도록 평평하게 펴서 보여주기 위한 grid
배열을 참고하면 왼쪽위에서부터 오른쪽 아래로 읽어 나가는 것을 볼 수 있습니다.
matrix
에 값을 넣을때는 row,column를 이용해서 서브스크립트에 맞는 형태로 값을 넘겨주면 설정할 수 있습니다:
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2
이 두 문장은 우측 상단의 값([0, 1])을 1.5
로, 좌측 하단의 값([1, 0])을 3.2
로 설정합니다:
Matrix
의 서브스크립트의 getter와 setter는 모두 올바른 row
와 column
값이 들어오는지 체크하는 assertion을 포함하고 있습니다. 이 assertion들을 돕기 위하여 Matrix
는 자체적으로 indexIsValid
라는 convenience method를 가지고 있으며 이는 주어진 값이 matrix
의 범위를 넘어가는지 아닌지를 체크합니다:
func indexIsValidForRow(row: Int, column: Int) -> Bool {
return row >= 0 && row < rows && column >= 0 && column < columns
}
만일 matrix
의 경계를 넘어가는 값이 서브스크립트로 들어오게 된다면 assertion이 발생합니다:
let someValue = matrix[2, 2]
// this triggers an assert, because [2, 2] is outside of the matrix bounds
Translator : YoonJeong Kwon (hoppingbonny@naver.com)
하나의 클래스는 또다른 클래스의 메서드, 프로퍼티, 이 외에 다른 특징들을 상속받을 수 있다. 어떤 클래스가 다른 클래스로부터 상속받을 때, 상속받는 클래스를 하위클래스(subclass)라 하고 하위클래스가 상속하는 클래스를 상위클래스(superclass)라 한다. Swift에서 상속이란, 다른 타입의 클래스들과 차별화하는 기본적인 방법이다.
Swift에서 모든 클래스는 상위클래스에 속한 메서드, 프로퍼티, 서브스크립트들을 호출하고 접근할 수 있고, 해당 메서드, 프로퍼티, 서브스크립트들을 오버라이딩하여 자신의 행동을 재정의하거나 수정할 수도 있다. Swift는 오버라이드 정의가 상위클래스의 정의와 일치하는 지를 확인하여 오버라이드가 정확히 이뤄졌음을 보장해주기도 한다.
또한 모든 클래스에는 프로퍼티 값의 변화를 감지하기 위한 프로퍼티 관찰자(property observers)를 상속한 프로퍼티에 추가할 수도 있다. 프로퍼티 관찰자는 해당 프로퍼티가 stored 혹은 computed 인지에 관계없이 어떤 프로퍼티에도 추가할 수 있다.
베이스 클래스(Base Class)란, 어떤 클래스도 상속받지 않은 클래스를 말한다.
NOTE Swift 클래스들은 보편적인 베이스 클래스를 상속받지 않는다. 당신이 정의한 클래스가 상위클래스를 가지지 않는다면, 자동적으로 베이스 클래스가 된다.
아래 예제는 Vehicle
베이스 클래스를 정의한 것이다. 이 베이스 클래스는 두 개의 변수(numberOfWheels
와 maxPassengers
)를 선언하고 있고, 이 두 변수는 모든 vehicle에 대한 기본 속성이다. description()
에서 이 변수들을 사용하여, vehicle 특징에 관한 설명을 String
타입으로 리턴한다.
class Vehicle {
var numberOfWheels: Int
var maxPassengers: Int
func description() -> String {
return "\(numberOfWheels) wheels; up to \(maxPassengers) passengers"
}
init() {
numberOfWheels = 0
maxPassengers = 1
}
}
Vehicle
클래스는 프로퍼티들의 초기값을 설정하기 위해 생성자(intializer)를 정의하고 있다. 생성자는 [Intialization] 에서 상세하게 다루며, 여기서는 상속한 변수가 하위클래스에 의해 어떻게 바뀌는지를 설명하기 위해 간략하게만 소개하도록 한다.
당신은 어떤 타입의 새로운 인스턴스를 생성하기 위해 생성자를 사용한다. 비록 생성자가 메서드는 아니지만, 그것들은 인스턴스 메서드와 매우 유사한 문법으로 작성된다. 생성자는 새로운 인스턴스를 사용 가능한 상태로 준비하고, 인스턴스의 모든 변수들이 유효한 초기값을 갖도록 보장한다.
가장 단순한 형태의 생성자는 매개변수가 없는 인스턴스 메서드처럼 보이며, init
키워드를 사용해서 작성한다.
init() {
// perform some initialization here
}
Vehicle
의 새로운 인스턴스를 생성하려면, 타입명(TypeName)
과 빈 괄호를 사용하여 생성자를 호출한다.
let someVehicle = Vehicle()
Vehicle
생성자는 인스턴스의 프로퍼티에 초기값을 설정한다. (numberOfWheels = 0
, maxPassengers = 1
)
Vehicle
클래스는 임의의 vehicle에 대해 공통적인 특징들을 정의하지만, 그 자체로 충분하지는 않다. 좀 더 유용한 클래스로 만들기 위해 더 구체적인 종류의 vehicle을 표현하도록 Vehicle 클래스를 재정의할 필요가 있다.
하위클래스를 정의(subclassing)한다는 것은 기존 클래스에 기반을 둔 새로운 클래스를 생성하는 것이다. 하위클래스는 기존 클래스의 모든 특징을 상속받고, 그것들을 재정의할 수 있다. 또한 새로운 특징들을 하위클래스에 추가할 수 있다.
어떤 클래스가 상위클래스를 갖는다는 것을 나타내려면 본 클래스명 뒤에 콜론(:)과 상위클래스명을 차례로 적는다.
class SomeClass: SomeSuperclass {
// class definition goes here
}
다음 예제는 더 구체적인 vehicle의 Bicycle
을 정의한 것이다. 이 새로운 클래스는 Vehicle
이 가진 역량에 기반한다. 상위클래스명(Vehicle
)을 콜론으로 구분하여 하위클래스명(Bicycle
) 뒤에 놓음으로써 Bicycle과 Vehicle 관계를 나타낼 수 있다.
이는 다음와 같이 설명할 수도 있다.
"Bicycle
이라는 새로운 클래스를 정의하고, 이 클래스가 Vehicle
의 특징들을 상속받는다"
class Bicycle: Vehicle {
init() {
super.init()
numberOfWheels = 2
}
}
Bicycle
은 Vehicle
의 하위클래스이고, Vehicle
은 Bycicle
의 상위클래스이다. 새로운 Bicycle
클래스는 자동적으로 Vehicle
의 모든 특징들, maxPassengers
와 numberOfWheels
와 같은 프로퍼티들을 획득한다. Bicycle
클래스의 요구조건을 맞추기 위해 상위클래스의 특징을 딱 맞게 조정하고 새로운 특징을 추가할 수 있다.
또한 Bicycle
클래스는 자신에게 딱 맞춰진 특징들을 설정하기 위해 생성자를 정의한다. Bicycle
생성자는 super.init()
메서드를 통해 상위클래스인 Vehicle
의 생성자를 호출하고 있다. 이 때 Bicycle
이 상속한 모든 변수들을 수정하기 전에 Vehicle
에 의해 초기화된다.
NOTE Object-C와 달리, Swift에서 생성자는 디폴트로 상속하지 않는다. 더 많은 정보를 보려면, [Initializer Inheritance and Overriding]을 참고하도록 한다.
Vehicle
에 의해 초기화된 maxPassengers
의 디폴트값은 어떤 bicycle에도 적절한 값이므로 Bicycle
생성자에서 변경하지 않았다. 그러나 numberOfWheels
의 디폴트 값은 적절치 않아 2
로 새롭게 대체했다.
Bicycle
은 Vehicle
의 프로퍼티들만 상속받는 것이 아니라, 메서드도 상속받는다. Bicycle
의 인스턴스를 생성한 다음, 상속한 description()
을 호출하여 프로퍼티들의 값이 어떻게 변경되었는지를 확인할 수 있다.
let bicycle = Bicycle()
println("Bicycle: \(bicycle.description())")
// Bicycle: 2 wheels; up to 1 passengers
하위클래스는 또다른 하위클래스를 만들 수 있다.
class Tandem: Bicycle {
init() {
super.init()
maxPassengers = 2
}
}
위 예제는 2인용 tandem 자전거를 위한 Bicycle
의 하위클래스이다. Tandem
은 Bicycle
로부터 두 프로퍼티를 상속받고, Bicycle
은 그 두 프로퍼티를 Vehicle
로부터 상속받는다. Tandem
은 bicyle이기 때문에 바퀴의 숫자는 동일하다. 그러나 tandem 자전거를 만들기 위해 maxPassengers
값을 적절하게 변경하고 있다.
NOTE 하위클래스는 오로지 초기화하는 동안 상위클래스의 변수(variable properties)만 수정할 수 있다. 상속한 하위클래스의 상수(constant properties)는 변경할 수 없다.
Tandem
인스턴스를 생성하고 해당 인스턴스에 대한 설명을 출력하는 것은 해당 프로퍼티들이 어떻게 수정되었는지를 보여준다.
let tandem = Tandem()
println("Tandem: \(tandem.description())")
// Tandem: 2 wheels; up to 2 passengers
description()
는 Tandem
이 상속받은 것이다. 어떤 클래스의 인스턴스 메서드는 그 클래스의 모든 하위클래스들이 상속받는다.
하위클래스는 인스턴스 메서드, 클래스 메서드, 인스턴스 프로퍼티 혹은 서브스크립트에 대해 자신만의 커스텀 구현체를 제공할 수 있다. 그렇지 않았다면 상위클래스의 것들을 상속받을 것이다. 이를 오버라이딩(Overriding)이라 한다.
상속한 특징을 오버라이드하려면 오버라이딩한다는 의미로 override
키워드를 접두사로 붙인다. 그렇게 하는 것은 의도적으로 오버라이드를 했고 실수로 일치하는 않는 정의를 한 것이 아님을 분명하게 만든다. 우연히도 오버라이딩은 예기치 못한 행동을 일으킬 수도 있고, override
키워드가 없는 어떤 오버라이드는 코드가 컴파일될 때 에러로 인식될 수도 있다.
또한 override
키워드는 Swift 컴파일러에게 오버라이딩 클래스의 상위클래스(혹은 부모 클래스 중 하나)가 당신이 오버라이드한 것과 일치하는 선언을 갖고 있는지 확인하도록 즉각적인 명령을 내린다. 이러한 확인은 오버라이딩 정의가 올바르게 되었음을 확실하게 만든다.
메서드, 프로퍼티, 서브스크립트를 오버라이드해서 하위클래스를 만들 때 오버라이드의 일부분으로서 기존 상위클래스의 구현을 활용하는 것이 때때로 유용하다. 예를 들면, 기존 구현의 행동을 재정의할 수도 있고 상속한 변수에 변경된 값을 저장할 수 있기 때문이다.
적절한 위치에서 super
접두사를 가지고 상위클래스의 메서드, 프로퍼티, 서브스크립트에 접근할 수 있다.
someMethod()
가 오버라이드 되었을 때, 오버라이딩 메서드 내부에서 super.someMethod()
를 통해 상위클래스의 someMethod()
메서드를 호출할 수 있다. someProperty
가 오버라이드 되었을 때, 오버라이딩한 접근자(getter)와 설정자(setter) 내부에서 super.someProperty
를 통해 상위클래스의 someProperty
를 호출할 수 있다.someIndex
에 해당하는 서브스크립트가 오버라이드 되었을 때, 오버라이딩한 서브스크립트 내부에서 super[someIndex]
를 통해 상위클래스의 동일한 서브스크립트를 호출할 수 있다.하위클래스에서 특정한 목적에 맞는 메서드를 제공하거나 해당 메서드를 대체하려면, 상속한 인스턴스나 클래스 메서드를 오버라이드하면 된다.
다음 예제에서 Vehicle
의 하위클래스인 Car
를 정의하였고, 이 클래스는 Vehicle
로부터 description()
를 상속받아 오버라이드한다.
class Car: Vehicle {
var speed: Double = 0.0
init() {
super.init()
maxPassengers = 5
numberOfWheels = 4
}
override func description() -> String {
return super.description() + "; " + "traveling at \(speed) mph"
}
}
Car
는 Double
타입의 새로운 speed
변수를 선언하고 있다. 이 변수는 0.0
으로 초기화되었고 이는 "시간당 0마일"을 의미한다. 또한 Car
는 커스텀 생성자를 통해 maxPassengers
를 5
, numberOfWeels
를 4
로 설정한다.
Car
는 상속한 description()
을 오버라이드하되, Vehicle
의 description()
과 동일한 선언부를 가진다. 오버라이딩 메서드 정의 시, override
키워드를 접두사로 사용한다.
오버라이딩한 description()
이 완전한 커스텀 구현을 제공하지는 않는다. super.description()
을 호출하여 Vehicle
의 리턴값을 받아 사용하기 때문이다. 그 다음 car의 현재 속도 정보를 덧붙여 출력하고 있다.
Car
의 새로운 인스턴스를 생성하고 description()
결과를 출력해보면, 실제 출력 내용이 변경되었음을 확인할 수 있다.
let car = Car()
println("Car: \(car.description())")
// Car: 4 wheels; up to 5 passengers; traveling at 0.0 mph
프로퍼티에 대한 커스텀 접근자와 설정자를 제공하거나 프로퍼티 값의 변경을 감시하기 위한 프로퍼티 관찰자를 추가하려면, 상속한 인스턴스나 클래스 메서드를 오버라이드하면 된다.
상속한 프로퍼티를 오버라이드 하려면 그 프로퍼티가 stored 혹은 computed 프로퍼티인지에 관계없이 커스텀 접근자와 설정자를 제공하면 된다. 상속한 프로퍼티의 stored 혹은 computed 성질은 하위클래스는 알지 못하고, 오로지 상속한 프로퍼티의 이름과 타입만 알 뿐이다. 항상 오버라이딩 하려는 프로퍼티의 이름과 타입을 동일하게 유지해야 한다. 그래야 컴파일러가 오버라이드한 것과 상위클래스의 프로퍼티의 이름과 타입이 일치하는지를 체크할 수 있다.
하위클래스 프로퍼티를 오버라이딩할 때 접근자와 설정자를 동시에 정의함으로써 읽기만 가능했던 상속 프로퍼티를 읽고 쓰기가 가능한 프로퍼티로 나타낼 수 있다. 그러나 읽고 쓰기가 가능한 상속 프로퍼티를 읽기만 가능한 프로퍼티로 나타낼 수는 없다.
NOTE 프로퍼티 오버라이드 중 설정자를 제공한다면, 접근자도 반드시 제공해야 한다. 아래
SpeedLimitedCar
예제처럼 상속한 프로퍼티의 값을 오버라이딩 접근자 안에서 변경하고 싶지 않다면,super.someProperty
를 통해 상속한 프로퍼티의 값을 그대로 해당 접근자로부터 가져올 수 있다.
다음 예제는 Car
의 하위클래스인 새로운 SpeedLimitedCar
를 정의한 것이다.
SpeedLimitedCar
클래스는 속도 제한 장치가 장착된 차를 나타낸다. 속도 제한 장치는 40mph보다 빠르게 달리는 것을 방지한다. 이러한 제한규칙은 상속한 speed
프로퍼티를 오버라이딩함으로써 구현할 수 있다.
class SpeedLimitedCar: Car {
override var speed: Double {
get {
return super.speed
}
set {
super.speed = min(newValue, 40.0)
}
}
}
SpeedLimitedCar
인스턴스의 speed
프로퍼티를 설정할 때마다 프로퍼티의 설정자는 새로운 값을 확인하고 그 값을 40mph로 제한한다. 이는 상위클래스의 speed
프로퍼티에 새로 입력한 속도값
과 40.0
중 가장 작은 값을 대입함으로써 행해진다. min
함수에 의해 두 개의 값 중 가장 작은 것이 선택된다. min
함수는 글로벌 함수로 Swift 표준 라이브러리를 통해 제공되며, 두 개 이상의 값을 제공받아 가장 작은 값을 리턴한다.
SpeedLimitedCar
인스턴스의 speed
프로퍼티에 40mph 이상을 대입하고 description()
을 통해 결과를 출력해보면, 속도가 제한되었음을 확인할 수 있다.
let limitedCar = SpeedLimitedCar()
limitedCar.speed = 60.0
println("SpeedLimitedCar: \(limitedCar.description())")
// SpeedLimitedCar: 4 wheels; up to 5 passengers; traveling at 40.0 mph
프로퍼티 관찰자를 상속한 프로퍼티에 추가하려면, 프로퍼티 오버라이딩을 사용한다. 이것은 프로퍼티가 어떻게 구현되었는지에 관계없이 상속한 프로퍼티 값의 변화를 알아차릴 수 있도록 해준다. 프로퍼티 관찰자에 대한 더 많은 정보를 보려면, [Property Observers]를 참고하도록 한다.
NOTE 프로퍼티 관찰자는 상수 혹은 읽기 전용 프로퍼티에 추가될 수 없다. 이러한 프로퍼티 값은 다시 설정될 수 없기 때문에 오버라이드의 일부인
willSet()
혹은didSet()
을 제공하는 것은 적절치 않다. 또한 오버라이딩 설정자와 오버라이딩 프로퍼티 관찰자를 동시에 제공할 수 없다. 프로퍼티 값이 변경되는 것을 관찰하고 싶고 이미 그 프로퍼티를 위한 커스텀 설정자를 제공하고 있다면, 커스텀 설정자 안에서 값의 변화를 간단하게 관찰할 수 있다.
다음 예제는 Car
의 하위클래스인 AutomaticCar
를 정의한 것이다. AutomaticCar
클래스는 자동 기어박스를 가진 차를 나타내고, 자동 기어박스는 현재 속도에 따라 자동적으로 적절한 기어를 선택한다. 또한 AutomaticCar
의 커스텀 description()
는 현재 기어 정보를 포함하여 출력하도록 구현했다.
class AutomaticCar: Car {
var gear = 1
override var speed: Double {
didSet {
gear = Int(speed / 10.0) + 1
}
}
override func description() -> String {
return super.description() + " in gear \(gear)"
}
}
AutomaticCar
인스턴스의 speed
프로퍼티를 설정할 때마다, 프로퍼티의 didSet
관찰자는 새로운 속도 값에 따라 자동적으로 gear
프로퍼티에 적절한 기어값을 할당한다. 이 관찰자는 새로운 speed
값을 10
으로 나눈 후 반올림한 정수를 기어값으로 선택한다. speed가 10.0
이면 gear에1
이 할당되고, speed가 35.0
이면 gear에 4
가 할당된다.
let automatic = AutomaticCar()
automatic.speed = 35.0
println("AutomaticCar: \(automatic.description())")
// AutomaticCar: 4 wheels; up to 5 passengers; traveling at 35.0 mph in gear 4
메서드, 프로퍼티, 서브스크립트를 오버라이딩하지 못하도록 하려면, final로 표시하면 된다. @final
속성을 첫 키워드 앞에 표시한다. (예, @final var
, @final func
, @final class func
, @final subscript
)
하위클래스 내에 final 메서드, 프로퍼티, 서브스크립트를 오버라이드하면 컴파일 시간 에러(compile-time error)를 발생한다. 확장 클래스 안에 추가한 메서드, 프로퍼티, 서브스크립트도 final로 표시될 수 있다.
전체 클래스를 final로 만들려면 class
키워드 앞에 @final
을 표시(@final class
)하면 된다. 하위클래스를 final 클래스로 만들려면 컴파일 시간 에러를 발생할 것이다.
Translator : Quartet ( ungsik.yun@gmail.com )
초기화는 클래스, 구조체, 또는 열거형의 인스턴스를 사용하기 위한 준비 과정입니다. 이 과정은 해당 인스턴스의 각각의 저장된 속성의 초기값을 설정하는 것과 그 외의 다른 설정 또는 새 인스턴스를 사용하기 전에 필요한 초기화를 합니다. 이 초기화 과정을 이니셜라이저(initializer)를 정의함으로서 구현할 수 있습니다. 이니셜라이저는 특정 타입의 새 인스턴스를 만들때 호출될 수 있는 특수 메소드입니다. 다른 오브젝티브 C의 이니셜라이저와는 달리 스위프트의 이니셜라이저는 값을 반환하지 않습니다. 이니셜라이저의 주 역할은 새 인스턴스가 처음 사용되기 전에 잘못된 곳이 없이 초기화가 되었는지 보장하는 것입니다. 또한 클래스 타입의 인스턴스는 디이니셜라이저(deinitializer)를 정의 할 수 있습니다. 디이니셜라이저는 할당 해제되기 바로 직전에 맞춤 정리를 수행합니다. 디이니셜라이저에 대해 더 많은 정보를 원하시면 Deinitialization을 보세요.
클래스와 구조체의 인스턴스가 생성될때에 맞춰서 인스턴스내의 저장된 속성은 적절한 초기값으로 설정이 되어야 합니다. 저장된 속성은 정해지지 않은 상태로 남아있을 수 없습니다. 이니셜라이저를 통해 저장 속성에 초기값을 설정하거나, 속성의 정의의 일부분으로서 기본 속성값을 지정 할 수 있습니다. 이 행동들은 뒤따르는 섹션에 설명되어 있습니다.
NOTE 저장 속성에 기본값을 지정하거나, 이니셜라이저에서 초기값을 설정할 때, 어떠한 속성 감시자(observer)도 호출하지 않고 속성의 값이 직접 설정 됩니다.
이니셜라이져는 특정 타입의 새 인스턴스를 만들 때 호출됩니다. 제일 단순한 형태의 이니셜라이저는, init
키워드를 사용하며, 파라메터가 없는 인스턴스 메소드의 형태입니다.
밑의 예제는 Fahrenheit
구조체를 정의하여 화씨 단위로 표현된 온도를 저장합니다. Fahrenheit
구조체는 double
타입의 temperature
저장 속성 단 하나만을 가지고 있습니다.
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
println("The default temperature is \(f.temperature)° Fahrenheit")
// prints "The default temperature is 32.0° Fahrenheit"
이 구조체는 파라메터가 없는 단일 이니셜라이저 init
을 정의합니다. 이 이니셜라이져는 저장된 온도값을 화씨 단위로 표현했을때의 물의 어는점인 32.0으로 초기화합니다.
위에 보인 것처럼 이니셜라이저 안에서 저장 속성의 초기값을 설정할 수 있습니다. 또 다른 방법은, 속성 선언의 일부로 기본 속성 값을 지정하는 것입니다. 속성을 정의할때 초기 값을 속성에 할당 하는 것으로 기본 속성 값을 지정 할 수 있습니다.
NOTE 만약 속성이 언제나 똑같은 초기값을 가진다면, 이니셜라이저 안에서 값을 설정하기보다는 기본 값을 주는 것이 낫습니다. 결과적으로는 같지만, 기본값이 속성의 선언에 더 근접해서 속성의 초기화를 합니다. 이로써 더 짧고 명확한 이니셜라이저를 작성할 수 있게 하고, 기본 값에서 속성의 타입을 개발자가 유추할 수 있게 합니다. 또한 기본값은 이 장의 뒤에서 설명되겠지만, 기본 이니셜라이저의 장점을 취하는 것과 이니셜라이저 상속을 쉽게 합니다.
temperatur
속성을 선언할때 기본값을 제공하는 것을 통해, 위해서 보인 단순한 형태로 Fahrenheit
구조체를 다시 작성 할 수 있습니다.
struct Fahrenheit {
var temperature = 32.0
}
이 섹션에서 설명할 것은, 입력 파라메터와 옵셔널 속성 타입을 이용하거나, 상수 속성을 초기화 과정중에 변경하는 것으로 초기화 과정을 사용자가 정의하는 것 입니다.
사용자 정의 초기화의 타입들과 값의 이름들을 정의하기 위해 이니셜라이저의 정의중 일부분으로 초기화 파라메터를 제공할 수 있습니다. 초기화 파라메터는 함수나 메소드의 파라메터와 같은 기능과 문법을 가지고 있습니다.
다음의 예제는 섭씨 단위로 온도를 표현하여 저장하는 Celsius
구조체를 정의합니다. Celsius
구조체는 init(fromFahrenheit:)
과 init(fromKelvin:)
두개의 이니셜라이저를 구현하여 다른 온도 단위에서 값을 받아와 새 인스턴스를 초기화합니다.
struct Celsius {
var temperatureInCelsius: Double = 0.0
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0
첫번째 이니셜라이저는 하나의 초기화 파라메터 fromFahrenehit
를 외부 이름으로 가지고 fahrenheit
를 지역 이름으로 가집니다. 두번째 이니셜라이저는 하나의 초기화 파라메터 fromKelvin
을 외부 이름으로 가지고 kelvin
을 지 역 이름으로 가집니다. 두 이니셜라이저 모두 하나의 인자(argument)를 섭씨 단위로 변환해 temperatureInCelsius
라는 속성에 값을 저장합니다.
함수나 메소드의 파라메터처럼, 초기화 파라메터 또한 이니셜라이저의 안에서 쓰일 지역 이름과 이니셜라이저를 호출할때 쓸 외부 이름을 가질 수 있습니다.
하지만 이니셜라이저는 함수나 메소드처럼 괄호 앞에 있는 함수 이름으로 식별 가능한 이름을 가지고 있지 않습니다. 그러므로 이니셜라이저의 파라메터가 가지는 이름과 타입들을 어떤 이니셜라이저가 호출되는지 확인하는 특별히 중요한 역할을 합니다. 이 때문에 시위프트는 이니셜라이저의 사용자가 외부 이름을 지정하지 않은 모든 파라메터에 대해 자동 외부 이름을 부여합니다. 이 자동 외부 이름은 모든 초기화 파라메터에 해쉬 심볼(#
)을 붙인 것처럼, 지역 이름과 똑같이 지정 됩니다.
NOTE 만약 이니셜라이저의 파라메터의 외부 이름을 지정하고 싶지 않다면, 언더스코어 (
_
)를 해당 파라메터의 명시적 외부 이름으로 하여 위에 설명된 기본 행동을 덮어 씌우십시오.
다음 예제는 red
, green
, blue
를 상수 속성으로 가지는 Color
구조체를 정의합니다. 이 속성들은 0.0
부터 1.0
사이의 값을 저장하여 색안의 빨강, 초록, 파랑의 양을 나타냅니다.
Color
구조체는 Double
타입의 적절하게 이름지어진 파라메터 3개를 가지는 이니셜라이저를 제공합니다.
struct Color {
let red = 0.0, green = 0.0, blue = 0.0
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
}
새 Color
인스턴스를 만들때, 색의 세가지 구성요수를 외부 이름으로 사용하여 이니셜라이저를 호출 할 수 있습니다.
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
이니셜라이저를 호출할때 외부 이름을 사용하지 않고 호출 할 수 없음에 주의하십시오. 외부 이름은 이니셜라이저 안에 반드시 언제나 사용되어야 하며, 생략하게 되면 컴파일 타임 에러를 냅니다.
let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - external names are required
만약 저장 속성이 논리적으로 "값 없음"을 갖는게 허용이 된다면 - 어쩌면 초기화 과정중에 설정이 될 수 없다거나, 어느 순간 "값 없음"을 갖는게 허용이 되거나 - 그 속성을 옵셔널 타입으로 선언하십시오. 옵셔널 타입 속성은 자동적으로 nil
값으로 초기화가 됩니다. 그렇게 함으로써 해당 속성은 의도된 "아직 값 없음"을 초기화 과정중에 가지게 됩니다.
이어지는 예제는 response:
를 속성으로 갖는 SurveyQuestion
클래스를 정의합니다.
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
println(text)
}
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."
설문 조사의 대답은 설문을 하기 전까지 알 수 없습니다. 그래서 response
속성은 String?
타입 또는 "optinal String
" 타입입니다. 이는 SurveyQuestion
의 새 인트섵스가 초기화 되었을때 자동적으로 기본 값을 nil
로 할당하여 "no string yet"을 뜻하게 됩니다.
상수 속성이 명확한 값을 가지며 초기화가 끝나기 직전까지 상수 속성의 값을 초기화 과정중 언제라도 바꿀 수 있습니다.
NOTE 클래스 인스턴스는 상수 속성의 값을 오직 초기화 과정중에 해당 클래스에 의해서만 바꿀 수 있습니다. 상수 속성은 자식(sub) 클래스에 의해 변경될 수 없습니다.
위 예제의 SurveyQuestion
클래스의 text
속성을 상수 속성으로 바꾸어 재작성 할 수 있습니다. 질문은 한번 SurveyQuestion
클래스가 생성되고 나면 변경 될 수 없다는 것을 알리기 위해서죠. text
속성이 지금은 상수라 할지라도, 클래스의 이니셜라이저 안에서는 여전히 변경될 수 있습니다.
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
println(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
스위프트는 기본값을 모든 속성에 지정했지만 이니셜라이저를 가지지 않은 구조체나 베이스 클래스에 대해 기본 이니셜라이저를 제공합니다. 기본 이니셜라이저는 단순히 새 인스턴스를 만들고, 속성들을 각각의 기본값으로 설정합니다.
이 예제는 구매 목록안의 아이템의 이름, 수량, 구매 상태를 캡슐화하는 ShoppingListItem
클래스를 정의합니다.
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
ShoppingListItem
클래스의 모든 속성이 기본 값을 가지고 있고, 부모(super) 클래스가 없는 베이스 클래스이기 때문에, ShoppingListItem
은 자동적으로 기본 이니셜라이저를 구현하여 새 인스턴스가 생길때 속성들을 기본값으로 설정해줍니다. (name
속성은 온셔널 String
속성이어서 별달리 값이 코드에 쓰여있지 않아도 자동적으로 기본 값으로 nil
을 받습니다.) 위의 예제에서 ShoppingListItem
클래스는 기본 이니셜라이저와 ShoppingListItem()
이라 쓰여진 것처럼 이니셜라이저 문법을 이용하여 새 클래스 인스턴스를 만드는데 사용합니다. 만들어진 새 인스턴스는 item
변수에 할당 됩니다.
위에 언급된 기본 이니셜라이저 외에도, 구조체 타입은 자동적으로 멤버 단위 이니셜라이저를 부여받습니다. 구조체의 모든 저장 속성에 기본값이 제공되었지만 사용자 정의 이니셜라이저가 정의되지 않았을때 말이죠.
멤버 단위 이니셜라이저는 새 구조체 인스턴스의 멤버 속성을 초기화 하는 단축 표현(shorthand)입니다. 새 인스턴스의 속성들의 초기값은 멤버 단위 이니셜라이저의 이름을 통해 전달 될 수 있습니다.
밑의 예제는 Size
구조체를 width
와 height
속성 두개를 정의합니다. 두 속성은 전부 0.0
이 할담 됨으로써 Double
타입임이 암시됩니다.
두 속성 전부 기본값을 가지기에 Size
구조체는 자동적으로 Size
인스턴스를 초기화 할 수 있는 init(width:heigh:)
멤버 단위 이니셜라이저를 부여받게 됩니다.
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
이니셜라이져는 인스턴스 초기화 수행의 일부로 다른 이니셜라이저를 호출 할 수 있습니다. 이 과정은 이니셜라이저 델리게이션이라 하며, 여러 이니셜라이저 사이의 중복 코드를 피할 수 있게 합니다.
값 타입과 클래스 타입에 따라 이니셜라이져 델리게이션이 어떤 형태로 허용 되는가, 어떻게 작동하는지 그 규칙은 어떤가가 다릅니다. 구조체나 열거형과 같은 값 타입은 상속을 지원하지 않습니다. 그렇기에 이니셜라이저 델리게이션의 과정은 비교적 간단합니다. 그저 다른 이니셜라이저가 제공한 것만을 대리 수행(delegation)하면 되기 때문입니다. 하지만 클래스는 상속에서 설명한 것처럼, 다른 클래스에서 상속 받을 수 있습니다. 이는 곧 클래스는 상속 받은 저장 속성들이 초기화 과정중에 올바르게 할당 되었는지 보장해야 하는 추가적인 책임이 있다는 것을 뜻합니다. 이러한 책임은 밑의 클래스 상속과 초기화에 설명되어 있습니다.
값 타입에서 사용자 정의 이니셜라이저를 작성할때, 다른 같은 타입의 다른 이니셜라이저를 참조하려면 self.init
을 사용해야 합니다. 이니셜라이저 안에선 오직 self.init
만 호출 할 수 있습니다.
만약 값 타입의 사용자 정의 이니셜라이저를 정의한다면, 더 이상 해당 값 타입의 기본 이니셜라이저에 접근 할 수 없게 됩니다. (그게 구조체라면 멤버 단위 구조체 이니셜라이저도 포함합니다.)
이 제약으로 인해, 필수적인 설정을 하는 이니셜라이저 대신 의도치 않게 자동 기본 이니셜라이저를 실행함으로써일어 날 수 있는 문제를 방지합니다.
NOTE 만약 사용자 정의 값 타입이 기본 이니셜라이저와 멤버 단위 이니셜라이저, 그리고 사용자 정의 이니셜라이저를 동시에 쓰길 원한다면 이니셜라이져를 값 타입의 원래 구현의 부분으로 작성하기 보다 확장(extension)으로 작성하십시오. 자세한 정보는 확장을 보세요.
다음 예제는 사용자 정의 Rect
구조체를 정의하여 기하학적 사각형을 표현합니다. 이 예제는 Size
와 Point
, 두개의 지지(supporting) 구조체를 요구합니다. 두 구조체 모두 속성들의 기본값으로 0,0
을 제공합니다.
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
Rect
구조체는 세가지 방법중 하나로 초기화 될 수 있습니다. 기본값인 0으로 초기화된 origin
과 size
속성 값을 이용하여, 특정 기점(origin point)과 사이즈를 제공하여, 특정 중앙점과 사이즈를 제공하여. 이 초기화 옵션들은 Rect
정의 안에서 사용자 정의 이니셜라이저로서 표현됩니다.
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
첫번째 Rect
이니셜라이저인 init()
은 기능적으로 사용자 정의 이니셜라이저를 가지지 않을때의 기본 이니셜라이저와 똑같습니다. 이 이니셜라이저는 빈 몸체를 가지며, 빈 중괄호 한쌍 {}
으로 표현됩니다. 또한 아무런 초기화도 수행하지 않습니다. 이 이니셜라이져를 호출하면 Rect
인스턴스를 반환하며, 그 인스턴스의 origin
과 size
는 모두 속성에서 정의된 기본값인 Point(x: 0.0, y:0,0)
과 Size(width: 0.0, height: 0.0)
입니다.
let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)
두번째 Rect
이니셜라이져인 init(origin:size:)
은 기능적으로 사용자 정의 이니셜라이저를 가지지 않을때의 멤버 단위 이니셜라이져와 동일합니다. 이 이니셜라이져는 단순히 origin
과 size
인수를 알맞은 저장 변수에 할당합니다.
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)
세번째 Rect
이니셜라이저인 init(center:size:)
은 조금 더 복잡합니다. center
포인트와 size
에서 계산한 적절한 기점에서 시작하게 됩니다. 그리고 나면 init(origin:size:)
이니셜라이져를 호출( 혹은 대리)합니다. 그 이니셜라이져는 알맞은 새 기점과 사이즈 값을 저장합니다.
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
init(center:size:)
이니셜라이저는 새 origin
과 size
값을 적절한 속성에 할당받게 할 수 있습니다. 하지만 init(center:size:)
이니셜라이저를 이용하는게 더 편하고 의도가 명확하며, 이미 있는 이니셜라이저가 제공하는 기능을 활용할 수 있는 장점이 있습니다.
NOTE
init()
과init(origin:size:)
을 작성하지 않고 위의 예제를 작성해보려면 확장을 보세요.
클래스의 모든 저장 속성은 - 부모 클래스에서 상속받은 어떠한 속성또한 포함하여 - 반드시 초기화 과정중에 초기 값을 할당받아야 합니다. 스위프트는 두 종류의 이니셜라이져를 제공하여 클래스 타입의 모든 저장 속성이 초기값을 갖게끔 보장합니다. 각각 지정 이니셜라이저와 편의 이니셜라이저라 합니다.
지정 이니셜라이저는 클래스의 주 이니셜라이저입니다. 지정 이니셜라이저는 해당 클래스에서 접하는 모든 속성을 완전히 초기화하고, 적절한 부모 클래스 이니셜라이저를 호출하여 초기화 과정을 부모 클래스로 연쇄시킵니다. 클래스들은 매우 적은 수의 지정 이니셜라이저를 가지는 경향이 있으며, 보통의 경우 클래스는 오직 하나만 가집니다. 지정 이니셜라이저는 초기화가 이루어지는 곳의 "깔때기" 지점이며, 부모 클래스로 이어지는 초기화 과정 연쇄의 "깔때기" 지점입니다. 모든 클래스는 반드시 최소한 하나의 지정 이니셜라이저를 가져야합니다. 때때로, 자동 이니셜라이저 상속에 설명된 것과 같이, 이 조건은 부모 클래스에서 하나 이상의 지정 이니셜라이저를 상속 받음에 따라 충족되는 경우가 있습니다. 편의 이니셜라이저는 클래스를 지탱하는 두번째 이니셜라이저입니다. 편의 이니셜라이저를 정의하여, 같은 클래스 내의 지정 이니셜라이저를 호출하는 편의 이니셜라이저를 만들 수 있습니다. 이 편의 이니셜라이져를 통해 호출하는 지정 이니셜라이저의 몇몇 파라메터를 기본 값으로 설정할 수 있습니다. 또한 편의 이니셜라이저를 정의하여 특정 쓰임새나 입력 값 타입에 대한 클래스 인스턴스를 만들 수 있습니다. 클래스가 필요로 하지 않는다면 편의 이니셜라이저를 제공할 필요는 없습니다. 편의 이니셜라이저는 보통 초기화 패턴을 단축하거나, 클래스의 의도를 명확하게 할때 만듭니다.
지정 이니셜라이저와 편의 이니셜라이저의 관계를 단순화 하기 위해, 스위프트는 다음의 3개 규칙을 이니셜라이저간의 델리게이션에 적용합니다.
간단히 기억하기 위한 방법은 이렇습니다.
이 규칙은 다음의 그림으로 표현될 수 있습니다.
여기 부모 클래스는 하나의 지정 이니셜라이저를 가지고 있으며, 두개의 편의 이니셜라이저를 가지고 있습니다. 한 편의 이니셜라이저는 다른 편의 이니셜라이저를 호출하며, 그 이니셜라니저는 하나 있는 지정 이니셜라이저를 호출합니다. 이는 위의 규칙 2와 3을 충족시킵니다. 부모 클래스는 더이상의 부모 클래스를 가지지 않기에 규칙 1은 적용되지 않습니다. 그림의 서브 클래스는 두개의 지정 이니셜라이져와 하나의 편의 이니셜라이저가 있습니다. 편의 이니셜라이저는 반드시 두 지정 이니셜라이저중 하나의 이니셜라이저를 호출해야 합니다. 편의 이니셜라이저는 클래스 안의 다른 이니셜라이저만 호출 가능하기 떄문입니다. 이는 위의 규칙 2와 3을 만족시킵니다. 규칙 1을 만족시키기 위해, 두개의 지정 이니셜라이저는 반드시 부모 클래스에 하나 있는 지정 이니셜라이저를 호출해야 합니다.
NOTE 이 규칙들은 각각의 클래스를 생성하는 방법에 영향을 주지 않습니다. 위 다이어그램의 어느 이니셜라이저라도 자기가 속해야 될, 완전히 초기화된 클래스 인스턴스를 만드는데 쓰일 수 있습니다. 이 규칙은 오직 클래스 구현의 작성에만 영향을 끼칩니다.
밑의 그림은 더 복잡한 4개의 클래스 간의 계층도를 나타냅니다. 이 그림은 지정 이니셜라이저가 어떻게 이 계층도의 클래스 초기화 과정에서 "깔때기"처럼 작동하고, 연쇄에서 클래스간의 관계를 단순화하는지 보여줍니다.
스위프트의 초기화는 두 단계의 과정을 거칩니다. 첫번째 단계에서는 해당 클래스가 가지는 각각의 저장 속성에 초기값을 할당합니다. 모든 저장 속성의 초기 상태가 정해지고 나면, 두번째 단계가 시작됩니다. 두번째 단계에서는 클래스 인스턴스가 사용될 준비가 되기 전까지, (역주: 상속 트리 상에서의)각각의 클래스가 저장 속성을 사용자 정의할 기회를 가집니다. 이 단계 초기화 과정을 사용하는 것은 초기화를 안전하게 하면서도, 클래스 상속 계층 상에서 각각의 클래스가 완전한 유연성을 가지게 합니다. 이 단계 초기화는 속성 값이 초기화 되기 전에 접근되는 것을 방지하며, 다른 이니셜라이저에 의해 의도치 않게 다른 값이 설정되는 것을 방지합니다.
NOTE 스위프트의 이 단계 초기화 과정은 오브젝티브 C의 초기화와 비슷합니다. 주요한 차이점은 첫번째 단계에 있습니다. 오브젝티브 C는 0이나 널(null) 값(
0
또는nil
)을 모든 속성에 할당합니다. 스위프트의 초기화 흐름은 좀 더 유연하려 사용자 정의 초기값을 설정할 수 있게 해줍니다. 그리고0
이나nil
이 기본값으로 유효하지 않은 타입에 대처할 수 있게 합니다.
스위프트의 컴파일러는 이 단계 초기화가 에러없이 완료 될 수 있게 4가지 안전 점검(safety check)을 수행합니다.
위에 언급된 것처럼, 객체를 위한 메모리는 저장 속성의 초기 상태가 알려져야 완전히 초기화 되었다고 간주합니다. 이 규칙을 만족시키기 위해서 지정 이니셜라이저가 초기화 연쇄를 위로 전달하기 전에 자신의 속성이 초기화 되었음을 확실히 해야합니다.
안전 점검 2 지정 이니셜라이저는 상속받은 속성에 값을 할당하기 전에 부모 클래스의 이니셜라이저를 대리 수행해야 합니다. 만약 그렇게 하지 않는다면, 지정 이니셜라이저가 할당한 새 값은 부모 클래스의 초기화 과정중에 덮어 씌워질 것입니다.
안전 점검 3 편의 이니셜라이저는 (역주: 클래스 상속 계층상) 같은 클래스 내부에서 정의된 속성을 포함한, 어떤 속성에라도 값을 할당하기 전에 다른 이니셜라이저를 대리 수행해야 합니다. 그렇게 하지 않을 경우 편의 이니셜라이저가 할당한 새 값은 해당 클래스의 지정 이니셜라이저에 의해 덮어씌워질 것입니다.
self
를 초기화의 첫 단계가 끝나기 전에 참조할 수 없습니다.클래스 인스턴스는 첫 단계가 끝나기 전까지는 완전히 유효하지 않습니다. 첫 단계가 끝나고 클래스 인스턴스가 유효하다고 알려져야만 속성에 접근 가능하고, 메소르를 호출 할 수 있습니다.
여기서 어떻게 위의 4가지 안전 점검에 의한 두 단계 초기화가 진행되는지 설명합니다.
Phase 1
Phase 2
self
에 접근 가능하고, 자신의 속성을 변경하거나, 인스턴스 메소드를 호출하거나 할 수 있습니다.self
를 이용하여 작업할 수 있습니다.초기화 호출의 첫 단계가 어떻게 보이는지 가상의 자식 클래스와 부모 클래스를 이용하여 보여줍니다.
이 예제에서 초기화는 자식 클래스의 편의 이니셜라이저를 호출 하는 것으로 시작합니다. 이 편의 이니셜라이저는 아직 어떤 속성도 변경할 수 없습니다. 편의 이니셜라이저는 같은 클래스 안의 지정 이니셜라이저를 대리 실행합니다. 지정 이니셜라이저는 안전 점검 1에 따라, 모든 자식 클래스의 속성이 값을 가졌는지 확실히 합니다. 그 후에 지정 이니셜라이저는 부모 클래스의 지정 이니셜라이저를 불러 초기화 연쇄를 위로 올립니다. 부모 클래스의 지정 이니셜라이저는 부모 클래스의 속성이 모두 값을 가졌는지 확실히 합니다. 초기화를 해야할 부모 클래스가 없기 때문에, 더 이상의 대리 수행(delegation)은 필요치 않습니다. 부모 클래스의 속성들이 초기 값을 가지는 순간부터, 인스턴스의 메모리는 완전히 초기화가 되었다고 간주되며, 첫 단계는 끝이 납니다.
여기 두번째 단계가 같은 초기화 호출에서 어떻게 보이는지 설명이 있습니다.
부모 클래스의 지정 이니셜라이저는 인스턴스를 추가적으로 사용자 정의할 수 있는 기회가 있습니다. 물론 하지 않아도 됩니다. 부모 클래스의 지정 이니셜라이저가 종료되면, 자식 클래스의 이니셜라이저가 추가 사용자 정의를 수행할 수 있습니다. 이번에도 물론, 하지 않아도 됩니다. 마지막으로, 자식 클래스의 지정 이니셜라이저가 종료되면, 처음에 지정 이니셜라이저를 호출한 편의 이니셜라이저가 추가적인 사용자 정의를 수행합니다.
오브젝티브 C의 자식 클래스와는 다르게, 스위프트의 자식 클래스는 부모 클래스의 이니셜라이저를 기본적으로 상속받지 않습니다. 스위프트의 이러한 접근 방식은 부모 클래스의 단순한 이니셜라이저가 자동적으로 더 복잡한 자식 클래스에 상속되어, 자식 클래스의 새 인스턴스를 생성할때 완전하지 않게 또는 올바르지 않게 초기화되는 것을 방지합니다. 만약 자식 클래스가 부모 클래스와 똑같은 이니셜라이저를 하나 이상 가지게 하고 싶다면, - 아마도 약간의 사용자 정의를 초기화 과정중에 수행할 - 같은 이니셜라이져를 사용자 정의 자식 클래스에서 오버라이드 하려 구현해 제공할 수 있습니다. 만약 오버라이드 하려는 이니셜라이저가 지정 이니셜라이져라면, 그 구현을 자식 클래스에서 오버라이드합니다. 그리고 부모 클래스 버전의 이니셜라이저를 오버라이딩 버전에서 호출합니다. 만약 오버라이드 하려는 이니셜라이저가 편의 이니셜라이저라면, 오버라이드한 이니셜라이저는 반드시 해당 자식 클래스 안의 지정 이니셜라이저를 호출해야 합니다. 위의 이니셜라이저 연쇄에서 설명한 것처럼 말이죠.
NOTE 메소드, 속성, 서브스크립트와는 달리 이니셜라이는 오버라이드 할때
override
키워드가 필요하지 않습니다.
위에서 언급한 것처럼, 기본적으로 자식 클래스는 부모 클래스의 이니셜라이저를 상속받지 않습니다. 하지만 부모클래스의 이니셜라이저가 특정 조건을 만족한다면 자동적으로 상속이 됩니다. 실제로 이것이 뜻하는 바는 다음과 같습니다. 많은 일반적인 경우에 이니셜라이져를 오버라이드 할 필요가 없습니다. 또한 상속 받는 것이 안전할때, 부모 클래스의 이니셜라이져를 최소의 노력으로 상속 받을 수 있습니다. 자식 클래스에 의해 도입된 새로운 어떤 속성이라도 기본 값을 제공받는 다고 가정했을때, 다음의 두 규칙이 적용됩니다.
Rule 1 자식 클래스가 어떠한 지정 이니셜라이저도 정의하지 않았을 경우, 부모 클래스의 지정 이니셜라이저를 자동적으로 상속 받습니다.
Rule 2 자식 클래스가 부모 클래스의 모든 지정 이니셜라이저를, 위의 규칙 1에 의해 지정 이니셜라이저를 상속받아서 구현하든가, 자식 클래스 정의의 일부로서 사용자 정의 구현으로 제공한다면, 부모 클래스의 모든 편의 이니셜라이저를 자동적으로 상속받게 됩니다.
이 규칙은 자식 클래스가 추가적인 편의 클래스를 더할때도 적용이 됩니다.
NOTE 자식 클래스는 규칙 2를 만족시키는 것의 일부로서 부모 클래스의 지정 이니셜라이저를 자식 클래스의 편의 이니셜라이저로 구현할 수 있습니다.
클래스의 지정 이니셜라이저는 값 타입을 위한 단순 이니셜라이저와 같은 방식으로 작성됩니다.
init(parameters) {
statements
}
편의 이니셜라이저는 init
키워드 앞에 convenience
키워드를 공백으로 구분하여 위치하게 하는 것을 제외하면 같은 방식으로 작성됩니다.
convenience init(parameters) {
statements
}
다음 예제는 이정 이니셜라이저와 편의 이니셜라이저, 그리고 자동적 이니셜라이저 상속을 실제로 해몹니다. 이 예제는 Food
, RecipeIngredient
그리고 ShoppingListItem
클래스들의 상속 계층을 정의합니다. 그리고 클래스들 간의 이니셜라이저가 어떻게 상호작용하는지 보여줄 것입니다.
상속 계층의 베이스 클래스는 Food
입니다. 이 클래스는 음식의 이름을 캡슐화 합니다. Food
클래스는 String
타입의 name
속성을 도입합니다. 그리고 Food
인스턴스를 생성하는데 두 개의 이니셜라이저를 제공합니다.
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
밑의 그림은 Food
클래스의 이니셜라이저 연쇄가 어떻게 되는지 보여줍니다.
클래스는 기본 멤버 단위 이니셜라이저를 가지고 있지 않습니다. 그리고 Food
클래스는 단일 인자 name
을 받는 지정 이니셜라이저를 제공합니다. 이 이니셜라이저는 특정 이름으로 Food
인스턴스를 생성하는데 사용될 수 있습니다.
let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"
Food
클래스가 제공하는 init(name: String)
이니셜라이저는 Food
인스턴스의 모든 저장 속성이 완전히 초기화 되는 것을 보장하기에 지정 이니셜라이저 입니다. Food
클래스는 부모 클래스를 가지지 않습니다. 또한 init(name: String)
이니셜라이저도 초기화를 완료하기 위해 super.init()
을 호출할 필요가 없습니다.
Food
클래스는 인자가 없는 init()
편의 이니셜라이저 또한 제공합니다. init()
이니셜라이저는 Food
클래스의 init(name: String)
을 대리하여, [Unnamed]
값을 가진 name
을 이름의 기본 플레이스홀더(placeholder)로서 제공합니다.
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
클래스 상속 계층에서 두번째 클래스는 Food
클래스의 자식 클래스인 RecipeIngredient
입니다. RecipeIngredient
클래스는 요리 조리법의 재료를 모델링합니다. 이 클래스는 (Food
에서 상속받은 name
속성에 더해) Int
타입의 quantity
속성을 도입합니다. 그리고 RecipeIngredient
를 생성하기 위한 이니셜라이저 두개를 제공합니다.
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
밑의 그림은 RecipeIngredient
의 이니셜라이저 연쇄가 어떠한지 보여줍니다.
RecipeIngredient
클래스는 한개의 지정 이니셜라이저 init(name: String, quantity: Int)
을 가지고 있습니다. 이 이니셜라이저는 새 RecipeIngredient
인스턴스의 모든 속성을 채우는데 쓰입니다. 이 이니셜라이저는 RecipeIngredient
클래스가 도입한 유일한 새 속성인 quantity
속성에 넘겨받은 quantity
인자를 할당하는 것으로 시작합니다. 그리고 위의 이니셜라이저는 Food
클래스의 init(name: String)
이니셜라이저를 대리 수행합니다. 이 과정은 위의 이 단계 초기화에 다온 안전 점검 1을 만족합니다.
RecipeIngredient
는 편의 이니셜라이저 init(name: String)
또한 정의합니다. 이 이니셜라이저는 넘겨받은 이름을 가진 RecipeIngredient
인스턴스를 생성합니다. 이 편의 이니셜라이저는 생성될때 명시적인 재료 수량이 정해지지 않았다면 갯수가 1개라고 간주합니다. 이 편의 이니셜라이저를 정의함으로써 RecipeIngredient
인스턴스를 빠르고 더 편하게 생성 할 수 있습니다. 그리고 재료 수량이 한개인 RecipeIngredient
인스턴스를 몇개 만들때의 코드 중복을 피하게 합니다. 이 편의 이니셜라이저는 단순히 클래스의 지정 이니셜라이저를 대리합니다.
눈여겨 봐야 할 것은, RecipeIngredient
이 제공하는 init(name: String)
편의 이니셜라이저가, Food
의 지정 이니셜라이저인 init(name: String)
과 같은 파라메터를 받는다는 것 입니다. RecipeIngredient
가 이 이니셜라이저를 편의 이니셜라이저로서 제공한다고 해도, RecipeIngredient
는 부모 클래스의 모든 지정 이니셜라이저를 제공한 셈이 됩니다. 따라서 RecipeIngredient
는 자동적으로 부모 클래스의 편의 이니셜라이저를 모두 상속받게 됩니다.
이 예제에서 RecipeIngredient
의 부모 클래스인 Food
는 하나의 편의 이니셜라이저 init()
을 제공합니다. 따라서 이 이니셜라이저는 RecipeIngredient
에게 상속되게 됩니다. init()
의 상속된 버전은 정확하게 Food
버전과 똑같은 기능을 합니다. 대리 수행하는 이니셜라이저init(name: String)
을 Food
버전이 아니라 RecipeIngredient
버전을 사용하여 대리 한다는 것을 제외하면 말이죠.
이 세 이니셜라이저를 새 RecipeIngredient
인스턴스를 생성하는데 전부 사용할 수 있습니다.
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
클래스 상속 계층에서 세번째이자 마지막 클래스는 RecipeIngredient
의 자식 클래스인 ShoppingListItem
입니다. ShoppingListItem
는 구매 목록에 있는 조리법의 재료를 모델링합니다.
구매 목록에 있는 모든 품목(itme)들은 "unpurchased" 상태로 시작하게 됩니다. 이 사실을 표현하기 위해 ShoppingListItem
은 기본값을 false
로 가지는 puchased
불리언 속성을 도입합니다. ShoppingListItem
은 또한 ShoppingListItem
인스턴스를 글로 설명하기 위해 산출한 description
속성을 추가합니다.
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name.lowercaseString)"
output += purchased ? " ✔" : " ✘"
return output
}
}
NOTE
ShoppingListItem
은purchased
의 초기값을 제공하는 이니셜라이저를 정의하지 않습니다. 여기 모델링된 구매 목록에 추가되는 아이템은 언제나 구매되지 않은(unpurchased) 상태로 시작하기 때문입니다.
이 클래스가 도입한 모든 속성의 초기값을 제공하고, 어떤 이니셜라이저도 스스로 정의하지 않기 때문에 ShoppingListItem
은 자동적으로 모든 지정 이니셜라이저와 편의 이니셜라이저를 부모 클래스에서 상속받습니다.
밑의 그림은 전체 클래스 세개의 이니셜라이저 연쇄를 보여줍니다.
ShoppingListItem
인스턴스를 만드는데 세개의 상속받은 이니셜라이저 전부를 이용할 수 있습니다.
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
println(item.description)
}
// 1 x orange juice ✔
// 1 x bacon ✘
// 6 x eggs ✘
breakfastList
라는 새 배열은 배열 문자(array literal)로 만들어져 세개의 새 ShoppingListItem
인스턴스를 담습니다. 배열의 타입은 ShoppingListItem[]
가 될 것으로 추론하게 됩니다. 배열이 생성된 후, 배열의 첫번째 ShoppingListItem
의 이름은 "[Unnamed]"
에서 "Orange juice"
로 바뀌게 됩니다. 그리고 구매가 되었다고 표시하게 됩니다. 배열 안의 각 품목의 설명을 출력하게 하여 기대한 대로 기본 상태가 설정되었음을 보일 수 있습니다.
만약 저장 속성의 기본 값이 약간의 사용자 정의나 설정을 요구한다면, 클로저나 전역 함수를 이용하여 사용자 정의된 기본 값을 속성에 제공할 수 있습니다. 해당 속성이 속해있는 새 인스턴스가 초기화 될 때마다, 해당 클로저나 함수가 호출됩니다. 그리고 그 반환 값이 속성의 기본 값으로 사용됩니다.
그러한 클로저나 함수들은 대개 속성의 타입과 같은 임시 값을 만들어서, 그 값을 원하는 초기 상태로 맞춰주고, 그 임시 값을 속성의 기본 값으로 사용되게 반환합니다.
이 예제는 클로저가 어떻게 속성의 기본값을 제공할 수 있게 되는지 전체적인 뼈대를 보여줍니다.
class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}
클로저의 닫는 중괄호 바로 뒤에 빈 괄호 한쌍이 있는 것에 주의해 주십시오. 이는 스위프트에게 클로저를 즉시 실행 시키라고 지시합니다. 만약 이 괄호를 생략한다면 클로저의 반환 값이 아니라, 클로저 그 자체를 속성에 할당하려 시도하는 것이 됩니다.
NOTE 만약 클로저를 이용하려 속성을 초기화 하려고 한다면 다음을 기억해 두십시오. 클로저의 실행이 된 시점에서는 인스턴스의 나머지 부분은 아직 초기화가 되지 않은 상태입니다. 이는 클로저 안에서 다른 속성 값에 접근 할 수 없다는 것을 뜻합니다. 속성들이 기본값을 가지고 있다고 해도 말이죠. 또한 암시적
self
속성을 사용하거나, 인스턴스의 다른 메소드를 호출 할 수 없습니다.
이 예제는 Checkerboard
구조체를 정의하여 Draughts라고도 알려진 체커 게임의 보드를 모델링합니다.
체커 게임은 흑백 칸이 번갈아 있는 10 * 10 판 위에서 플레이합니다. 이 게임판을 표현하기 위해 Checkerboard
구조체는 길이가 100이고, Bool
값을 가지는 boardColors
라는 단일 속성을 가집니다. 배열에서 ture
값은 검은 칸을 표현하고, false
값은 흰색 칸을 표현합니다. 배열의 첫번째 아이템은 게임판에서 제일 좌상단의 칸을 표현하고, 마지막 아이템은 제일 게임판에서 우하단의 칸을 표현합니다.
boardColors
배열은 색상 값을 설정하기 위해 클로저를 사용하여 초기화가 됩니다.
struct Checkerboard {
let boardColors: Bool[] = {
var temporaryBoard = Bool[]()
var isBlack = false
for i in 1...10 {
for j in 1...10 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
return boardColors[(row * 10) + column]
}
}
새 Checkerboard
인스턴스가 생성될때, 해당 클로저가 실행되어 boardColors
의 기본 값이 계산되고 반환됩니다. 위 예제에서 보이는 클로저는 게임판 위의 각각의 칸에 알맞은 색을 계산하여 임시 배열인 temporaryBoard
에 설정합니다. 그리고 설정이 끝나면 이 임시 배열은 클로저의 반환값으로서 반환이 됩니다. 이 반환된 배열 값은 boardColors
에 저장이 되고, 기능성 함수 squareIsBlackAtRow
에 의해 조회 될 수 있습니다.
let board = Checkerboard()
println(board.squareIsBlackAtRow(0, column: 1))
// prints "true"
println(board.squareIsBlackAtRow(9, column: 9))
// prints "false"
Translator : 물좀 (메일주소)
deinitializer는 임의의 class 인스턴스가 할당해제(deallocate) 되기 직전에 호출된다. initializer를 init 키워드 안에 기술했던 것처럼, deinitializer는 deinit 키워드 안에 적어 넣는다. deinitializer는 class 타입 인스턴스에서만 사용할 수 있다.
Automatic Reference Counting 에서 설명된 바와 같이, Swift에서는 더 이상 사용되지 않는 인스턴스는 ARC에 의해 자동으로 할당해제 된다. 따라서 대부분의 경우 사용자가 직접 할당해제 할 필요가 없다. 하지만, 사용자의 인스턴스에서 직접 리소스를 할당하여 사용했다면, 해제할 때도 직접 해제해 주어야 한다. 가령, 사용자의 인스턴스에서 파일을 열어서 사용했다면, 해제할 때, 직접 파일을 닫아주어야 한다. class마다 오직 한 개의 deinitializer만을 사용할 수 있으며, 파라미터 없이 정의한다.
deinit {
// perform the deinitialization
}
deinitializer는 할당해제(deallocation)가 일어나기 직전에 자동으로 호출되기 때문에, 사용자가 직접 호출하는 것을 허용하지 않는다. subclass는 superclass의 deinitializer를 상속받기 때문에, subclass의 deinitializer가 호출되어 작업을 마친 후, superclass의 deinitializer가 자동으로 호출된다. superclass의 deinitializer는 subclass의 deinitializer가 정의되지 않았더라도 항상 호출된다. deinitializer가 아직 호출되지 않은 인스턴스는 해제(deallocation)가 되지 않은 상태이고, deinitializer는 자신이 속한 인스턴스의 모든 속성(가령, 닫아야할 파일의 이름과 상태)을 변경할 수 있다.
deinitializer를 사용하는 간단한 예를 들어보자. 간단한 게임을 만들기 위해, Bank와 Player 라는 두개의 데이터 타입을 정의하기로 하자. Bank는 화폐를 만드는데, 최대 10,000개의 동전을 유통할 수 있다. 게임에서는 오직 한 개의 Bank만 있다고 가정, Bank는 static으로 구현되었다.
struct Bank {
static var coinsInBank = 10000
static func vendCoins(var numberOfCoinsToVend: Int) -> Int {
numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receiveCoins(coins: Int) {
coinsInBank += coins
}
Bank클래스는 coinsInBank속성으로 현 상태의 동전 수를 유지한다. vendCoins과 receiveCoins 메소드는 동전을 인출하거나 예치할 때 사용한다.
vendCoins 메소드는 은행에 동전이 남아 있는지 확인하고 Player에게 인출을 허용한다. Player 가 요청한 것보다 동전이 적게 남아 있으면, 남아 있는 만큼만 인출할 수 있다. (물론 은행에 동전이 전혀 없다면, ‘0’ 를 리턴한다). numberOfCoinsToVend 변수는 파라미터로 입력받아 변경이 가능하게 만들었다. receiveCoins메소드는 단순히 Player가 예치하는 동전을 은행에 더해서 쌓도록 되어 있다.
Player 클래스를 보면, coinsInPurse 속성에 게임 플레이어가 현재 보유한 동전을 기록한다.
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.vendCoins(coins)
}
func winCoins(coins: Int) {
coinsInPurse += Bank.vendCoins(coins)
}
deinit {
Bank.receiveCoins(coinsInPurse)
}
}
Player인스턴스는 은행으로부터 동전을 받으면서 초기화된다. 경우에 따라 은행에 충분한 코인이 남아 있지 않다면 요청한 만큼의 동전보다 적게 받을 수도 있다. winCoins 메소드는 은행에서 (coins : Int) 만큼의 동전을 받아 Player의 지갑에 더해 준다. Player클래스에는 deinitializer 가 정의되어 있는데, 앞서 설명한 바 처럼 Player 인스턴스가 해제(deallocate)되기 직전에 호출된다. 여기서는 단순히 플레이어가 가진 모든 동전을 다시 은행으로 되돌려 보내는 작업을 한다.
**var playerOne: Player? = Player(coins: 100)
println("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
**_// prints "A new player has joined the game with 100 coins"_**
println("There are now \(Bank.coinsInBank) coins left in the bank")
**_// prints "There are now 9900 coins left in the bank"_**
Play 인스턴스를 만들때, 은행에 100 코인을 요청한다. 이 Player 인스턴스는 optional 변수 playerOne에 저장된다. 여기서 optional 변수가 사용된 이유는 게임 player들이 수시로 게임에서 나갈 수 있기 때문이다. optional을 사용함으로써 그 인스턴스가 현재 게임에 있는지 아닌지를 추적할 수 있다.
또한 '!' 연산자를 사용, coinsInPurse 가 호출될때, optional으로 선언된 기본값 (100) 대신 현재 인스턴스가 가지고 있는 값 (2100)이 인쇄되게 할 수도 있다.
playerOne!.winCoins(2000)
println("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
_// prints "PlayerOne won 2000 coins & now has 2100 coins"_
println("The bank now only has \(Bank.coinsInBank) coins left")
_// prints "The bank now only has 7900 coins left"_
결과를 보면, playerOne은 요청한 2000 코인 모두를 은행으로부터 받아서, 현재는 2100 코인을 가지고 있으며 은행에 남아 있는 코인 수는 7900이 되었다.
playerOne = nil
println("PlayerOne has left the game")
// prints "PlayerOne has left the game"
println("The bank now has \(Bank.coinsInBank) coins")
// prints "The bank now has 10000 coins"
playerOne을 nil로 셋팅함으로써, playerOne이 게임에서 나간 상태를 표현할 수 있으며, playerOne이 가지고 있던 코인은 모두 은행에 환수되었음을 알 수 있다. 그 과정은 playerOne 변수의 참조(reference)는 더 이상 유효하지 않게 되고, denitializer가 호출되고, deallocation 과정으로 사용중이던 메모리도 환원된다.
Translator : Quartet ( ungsik.yun@gmail.com )
스위프트는 앱의 메모리 사용량을 추척, 관리하기 위해 자동 참조 계수(ARC)를 사용합니다. 대부분의 경우, 이러한 메모리 관리는 스위프트에서 "그냥 잘 작동합니다". 개발자가 메모리 관리에 대해서 생각할 필요가 없다는 것이죠. ARC는 인스턴스가 더이상 필요가 없을때 해당 클래스 인스턴스가 쓰는 메모리를 자동으로 해제합니다. 하지만 때때로 ARC는 메모리 관리를 하기 위해서 코드 부분들간의 관계 정보를 알아야 할 때가 있습니다. 이번 장은 그러한 상황을 설명하고, 어떻게 ARC가 앱의 메모리 관리를 가능하게 하는지 보여줍니다.
NOTE 참조 계수는 오직 클래스의 인스턴스에만 적용됩니다. 구조체와 열거형은 값(Value) 타입이며, 참조 타입이 아닙니다. 또한 참조형태로 저장되거나 전달되지 않습니다.
클래스의 새 인스턴스를 만들때마다 인스턴스에 대한 정보를 저장하기 위해 ARC는 메모리 덩어리들을 할당합니다. 이 메모리는 인스턴스 타입에 관련된 정보와, 인스턴스와 관련된 저장 속성의 값들을 저장합니다. 그리고 ARC는 더이상 필요하지 않은 인스턴스의 메모리 할당을 해제하여 메모리가 다른 목적으로 이용될 수 있게 합니다. 이로써 클래스 인스턴스가 필요하지 않은 메모리를 차지하고 있는 것을 방지합니다. 하지만 만약 사용중인 인스턴스를 ARC가 할당 해제하면, 해당 인스턴스의 속성에 접근하거나 메소드를 호출하는 것은 불가능해집니다. 사실, 만약 그 인스턴스에 접근하려하면 앱은 크러시(Crash)가 날것입니다. 사용되고 있는 인스턴스가 사라지지 않게 하기 위해 ARC는 얼마나 많은 속성, 상수, 변수들이 각각의 클래스 인스턴스들을 참조하는지 추적합니다. 최소한 하나의 활성화 참조가 있는 이상, ARC는 해당 인스턴스의 할당을 해제하지 않습니다. 이를 가능하게 하기위해, 클래스 인스턴스를 속성, 상수, 변수에 할당할때 해당 속성, 상수, 변수는 해당 인스턴스에 강한 참조(Strong reference)를 합니다. 이 참조는 "강한" 참조라 불리는데, 해당 인스턴스를 강력하게 유지하기 때문입니다. 그리고 이 강한 참조가 남아있는 이상 해당 인스턴스의 할당 해제는 허용되지 않습니다.
여기 자동 참조 계수가 어떻게 작동하는지에 대한 예제가 있습니다. 이 예제는 name
이라는 저장된 상수 속성을 정의하는 단순한 클래스 Person
을 보여줍니다.
class Person {
let name: String
init(name: String) {
self.name = name
println("\(name) is being initialized")
}
deinit {
println("\(name) is being deinitialized")
}
}
Person
클래스는 name
속성을 설정하고 초기화가 진행중임을 알리는 메시지를 출력하는 이니셜라이져(initializer)를 가지고 있습니다. 또한 Person
클래스는 인스턴스가 할당 해제될 때 메시지를 출력하는 디이니셜라이져(deinitializer)를 갖고 있습니다.
다음 코드 조각들은 Person?
타입의 변수 3개를 정의하고 있습니다. 이 뒤에 Person
의 새 인스턴스들의 복수 참조에 사용하기 위한 것입니다. 타입은 Person
이 아닌 Person?
인 옵셔널(Optional) 타입이기 때문에, 변수들은 자동적으로 nil
로 초기화가 되며, 지금은 Person
인스턴스를 참조하지 않습니다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
이제 새로운 Person
인스턴스를 생성하여 변수 3개중에 하나에 할당할 수 있습니다.
reference1 = Person(name: "John Appleseed")
// prints "John Appleseed is being initialized"
"John Appleseed is being initialized"
라는 메시지가 Person
클래스의 이니셜라이져가 호출될 때 출력된다는 점에 주의합니다. 이것으로 초기화가 제대로 됐음을 확인할 수 있습니다.
reference1
변수에 Person
의 새 인스턴스가 할당 되었기 때문에, reference1
과 Person
인스턴스 사이에 강한 참조가 생깁니다. 그리고 최소한 하나의 강한 참조가 있어서 ARC는 Person
이 메모리에 유지되는 것과, 할당 해제 되지 않음을 확인 합니다.
만약 같은 Person
인스턴스를 두개 변수에 더 할당하면, 두개의 강한 참조가 더 생깁니다.
reference2 = reference1
reference3 = reference1
이제 하나의 Person
인스턴스에 대한 강한 참조는 3개입니다.
원래의 참조를 포함한 변수들 중에 nil
을 2개 할당함으로써 2개의 강한 참조를 부순다면, 하나의 강한 참조가 남게 되며, 여전히 Person
인스턴스는 할당해제 되지 않습니다.
reference1 = nil
reference2 = nil
세번째 강한 참조가 사라져 명확하게 Person
인스턴스가 더 이상 사용되지 않기 전까지 ARC는 Person
인스턴스를 할당 해제 하지 않습니다.
reference3 = nil
// prints "John Appleseed is being deinitialized"
위 예제에서 ARC는 생성된 Person
인스턴스의 참조 갯수를 추적하고 해당 Person
인스턴스가 더이상 필요하지 않을때 할당 해제를 합니다.
하지만 절대로 강한 참조의 갯수가 0으로 떨어지지 않게 코드를 작성하는 것이 가능합니다. 두개의 클래스 인스턴스가 서로를 강하게 잡고 있을때 그 현상이 발생합니다. 인스턴스 서로가 서로를 살게끔 유지하는 것이죠. 이를 강한 참조 순환(strong referecne cycle)이라고 합니다.
강한 참조 순환을 풀려면 클래스간의 관계를 강한 참조 대신 약한(weak) 참조나 미소유 참조(unowned references)로 대체해야 합니다. 이 과정은 Resolving Strong Reference Cycles 에 설명이 되어있습니다. 하지만 강한 참조 순환을 푸는걸 배우기 전에, 어떻게 순환이 생기는지 이해하는것이 좋습니다.
이 예제는 강한 참조 순환이 어떻게 의도치 않게 생기는지 보여줍니다. 이 예제는 아파트 블록과 거기에 사는 사람을 모델링하는 Person
과 Apartment
두개의 클래스를 정의합니다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}
모든 Person
인스턴스는 String
타입의 name
속성을 가지고 있고, 추가적으로 apartment
속성을 최초에 nil
값이 할당된 채로 가집니다. apartment
속성은 옵셔널입니다. 어떤 사람은 아파트에 살지 않을 수도 있기 때문입니다.
비슷하게, 모든 Apartment
인스턴스는 Int
타입의 number
속성을 가지고 있고, 추가적으로 최초에 nil
이 할당된 tenant
속성을 가지고 있습니다. tenant
속성은 옵셔널입니다. 어떤 아파트는 사람이 살지 않을 수도 있기 때문입니다.
두 클래스 전부 디이니셜라이저를 정의하여 클래스 인스턴스가 디이니셜라이(역주: 혹은 할당 해제) 된다는 사실을 출력하고있습니다. 이로 인해 Person
과 Apartment
인스턴스가 기대한대로 할당 해제가 되는걸 볼 수 있습니다.
다음 코드 조각은 john
과 number73
이라는 변수를 정의하고 있습니다. 이 변수들에 밑의 Apartment
와 Person
인스턴스를 설정할겁니다. 두 변수는 옵셔널이기에 초기값으로 nil
을 가집니다.
var john: Person?
var number73: Apartment?
이제 Person
과 Apartment
의 인스턴스를 생성해서 john
과 number73
변수에 할당 할 수 있습니다.
john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)
두 인스턴스를 생성 후에 할당하여 강한 참조가 어떻게 구성되는지 보여주는 그림입니다. john
변수는 새 Person
인스턴스에 강한 참조를 가지고 있으며 number73
변수는 Apartment
인스턴스에 강한 참조를 가지고 있습니다.
이제 두 인스턴스를 서로 연결하여 사람(person)이 아파트를 가지고, 아파트가 사람을 가지게 할 수 있습니다. 여기서 느낌표(!
)는 john
과 number73
인스턴스 안에 저장된 옵셔널(optional) 변수를 드러내어 접근할 수 있게 하는 것입니다. 그렇게 인스턴스의 속성은 다음과 같이 설정 될 수 있습니다.
john!.apartment = number73
number73!.tenant = john
여기 그림은 두 인스턴스간에 강한 참조가 어떻게 형성되어있는지를 보여줍니다.
안타깝게도 이러한 두 인스턴스간의 연결은 서로간의 강한 참조 순환을 발생시킵니다. Person
인스턴스는 Apartment
인스턴스에 대한 강한 참조를 가지고 있고, Apartment
인스턴스는 Person
인스턴스에 대한 강한 참조를 가지게 됩니다. 그러므로 john
과 number73
변수만을 이용하여 강한 참조를 없애려할때, 참조 계수는 0으로 떨어지지 않으며 ARC에 의해 인스턴스가 할당해제 되지 않습니다.
john = nil
number73 = nil
두 변수가 nil
로 할당 될 때 디이니셜라이저가 호출되지 않았음에 주의하세요. 강한 참조 순환은 Person
과 Apartment
의 인스턴스가 영원히 할당 해제 되지 않게하여 앱의 메모리 누수(leak)가 일어나게 합니다.
이 그림은 john
과 number73
변수가 nil
로 할당 된 후의 강한 참조가 어떻게 되었는지 보여줍니다.
Person
과 Apartment
간의 강한 참조는 여전히 남아있으며, 깨어질 수 없게 되었습니다.
스위프트는 약한 참조와 미소유 참조라는 2가지 방법 제공하여 클래스 속성에서 일어나는 강한 참조 순환을 해결할 수 있게합니다.
약한 참조나 미소유 참조는 참조 순환의 안에 있는 인스턴스가 다른 인스턴스에 대해 강한 참조를 유지할 필요 없이 참조할 수 있게 합니다. 인스턴스는 서로를 강한 참조 없이 참조 할 수 있게 됩니다.
약한 참조는 해당 참조가 살아있는 동안 잠시라도 nil
이 될때 사용하게 됩니다. 그와 반대로 미소유 참조는 참조가 초기화 과정 중 설정 되고 이후에 절대로 nil
이 되지 않음을 알고 있을 때 사용합니다.
약한 참조는 인스턴스가 다른 인스턴스를 참조하는데 강하게 유지하지 않는 참조이며, 그렇기에 ARC가 참조된 인스턴스를 버리는 것을 멈추게 하지 않습니다. 이로 인해 참조가 강한 참조 순환의 일부가 되는 것을 방지합니다. weak
키워드를 선언의 앞에 위치시키는 것으로 속성이나 변수 선언이 약한 참조라고 알릴 수 있습니다.
약한 참조는 참조가 어느 순간 "값 없음"을 참조하게 될때 사용되어 참조 순환을 피하는데 이용 됩니다. 만약 참조가 언제나 값을 가진다면 미소유 참조에 설명된 것처럼 미소유 참조를 대신 사용하면 됩니다. 위의 Apartment
예제에서는 아파트가 "거주자 없음" 상태를 가지는 것이 자연스럽기에 약한 참조를 사용하여 참조 순환을 부술 수 있습니다.
NOTE 약한 참조는 실행 시간중에 값이 바뀔 수 있기 때문에 반드시 변수로서 선언되어야 합니다. 약한 참조는 상수로 선언될 수 없습니다.
약한 참조는 "값 없음"을 가지는게 허용되기에, 약한 참조는 언제나 옵셔널 타입으로 선언되어야 합니다. 옵셔널 타입은 스위프트에서 "값 없음"을 표현하는데 선호되는 방식입니다.
약한 참조는 인스턴스를 강하게 참조 하고 있지 않기 때문에 약한 참조를 통해 참조를 하고 있는 동안 할당 해제가 될 가능성이 있습니다. 때문에 ARC는 약한 참조가 참조하고 있던 인스턴스가 할당 해제 되면 참조를 자동으로 nil
로 설정합니다. 다른 옵셔널 값들처럼, 약한 참조의 값이 존재하는지를 체크할 수 있습니다. 그렇기 때문에 존재하지 않는 잘못된 인스턴스를 참조하는 일은 일어나지 않습니다. (역주: 아예 nil
을 참조하는 것과, 있어야 할 자리에 엉뚱한게 있는 것을 참조 하는 것이 다르기에 위의 문장이 나온듯 싶습니다. C에서 포인터를 이용해 강제로 다른 부분을 읽는 것을 생각하면 될것 같습니다.)
밑의 예제는 위의 예제와 똑같지만 중요한 한가지가 다른 Person
과 Apartment
입니다. 이번에는 Apartment
타입의 tenant
속성이 약한 참조로 선언되어 있습니다.
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
weak var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}
john
과 number73
두 변수의 강한 참조와 두 인스턴스간의 연결은 이전엔 다음과 같았습니다.
var john: Person?
var number73: Apartment?
john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)
john!.apartment = number73
number73!.tenant = john
이 그림은 두 인스턴스의 현재 참조가 어떤지를 보여줍니다.
Person
인스턴스는 여전히 Apartment
인스턴스를 강한 참조로 하고 있습니다. 하지만 Apartment
인스턴스는 이제 Person
에 대해 약한 참조를 하고 있습니다. 이는 곧 john
변수에 대한 강한 참조를 없앴을때, Person
인스턴스에 대한 강한 참조가 없다는 것을 뜻합니다.
Person
인스턴스에 대한 강한 참조가 더이상 없기에 인스턴스는 할당해제 됩니다.
john = nil
// prints "John Appleseed is being deinitialized"
Apartment
인스턴스에 대한 강한 참조는 number73
변수에 대한 것밖에 남지 않았습니다. 그 강한 참조를 사라지게 한다면 Apartment
에 대한 강한 참조는 더이상 남아있지 않게 됩니다.
Apartment
에 대한 강한 참조가 더이상 없기 때문에, 이 인스턴스 역시 할당 해제 됩니다.
number73 = nil
// prints "Apartment #73 is being deinitialized"
위 두 코드 조각은 Person
과 Apartment
의 디이니셜라이저가 john
과 number73
변수가 nil
로 설정 될때 "디이니셜라이즈" 메시지를 출력하는 것을 보여줍니다. 이것으로 강한 참조가 사라졌을음 증명할 수 있습니다.
약한 참조처럼 미소유 참조 또한 인스턴스에 대한 참조를 강하게 하지 않습니다. 약한 참조와는 다르게, 미소유 참조는 언제나 값을 가지고 있다고 간주합니다. 이 때문에 미소유 참조는 옵셔널 타입이 아닙니다(non-optional). 미소유 참조는 unowned
키워드를 속성이나 변수 선언 앞에 위치 시킴으로써 할 수 있습니다.
미소유 참조는 옵셔널이 아니기 때문에 미소유 참조를 쓸 때마다 드러내야 할 필요가 없습니다. 미소유 참조는 언제나 직접 접근이 가능합니다. 하지만 ARC가 인스턴스의 참조를 할당 해제 할 때 nil
로 설정 할 수는 없습니다. 옵셔널이 아닌 타입은 nil
로 설정 될 수 없기 때문입니다.
NOTE 만약 미소유 참조가 참조하는 인스턴스가 할당 해제된 후에 접근하려 한다면 런타임 에러를 발생 시킬것입니다. 미소유 참조는 언제나 인스턴스를 참조하는 게 확실할 때에만 사용해야 합니다. 스위프트는 미소유 참조가 할당 해제된 인스턴스에 접근하려 할때 언제나 크래시를 낸다는 것에 주의하십시오. 앱은 언제나 안정적으로 크래시할 것입니다. 물론, 당연히 그런 일이 일어나지 않게 해야할테지만 말이죠.
다음의 예제는 Customer
와 CreditCard
두 클래스를 정의하고 있습니다. 이 클래스는 은행 고객과 그 고객에게 가능한 신용카드를 모델링합니다. 이 두 클래스는 서로의 인스턴스를 속성으로 저장합니다. 이 관계는 강한 참조 순환을 만들 가능성이 있습니다.
Customer
와 CreditCard
의 관계는 위의 약한 참조 예제에서 살펴본 Person
과 Apartment
의 관계와는 조금 다릅니다. 이 데이터 모델에서 고객은 신용 카드를 가질수도 있고 안가질수도 있습니다. 하지만 신용 카드는 언제나 고객과 연관이 됩니다. 그것을 표현하기 위해 Customer
클래스는 card
속성을 옵셔널 로 가지지만, CredicCard
클래스는 customer
를 논옵셔널(non-optional) 속성으로 가집니다.
게다가 새로운 CreditCard
인스턴스는 오직 number
값과 customer
인스턴스를 CreditCard
의 맞춤(custom) 이니셜라이저를 통해서만 생성될 수 있습니다. 이를 통해 CreditCard
인스턴스가 생성될 때는 언제나 credit
인스턴스와 연관이 됨을 보증할 수 있습니다.
신용카드는 언제나 고객을 가지기 때문에 customer
속성을 미소유 참조로 설정하여 강한 참조 순환을 피할 수 있습니다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { println("\(name) is being deinitialized") }
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized") }
}
다음 코드 조각은 옵셔널 Customer
변수인 john
을 정의하여 특정한 고객의 정보를 참조하게 하였습니다. 이 변수는 옵셔널 변수임으로 nil
을 초기값으로 갖습니다.
var john: Customer?
이제 Customer
인스턴스를 생성하여 인스턴스의 card
속성에 할당할 CreditCard
인스터스의 초기화에 이용할 수 있습다.
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
이 그림은 위 두 인스턴스간의 관계가 어떻게 되는지 보여주고 있습니다.
Customer
인스턴스는 CreditCard
에 대해 강한 참조를 하고 있습니다. CreditCard
는 Customer
인스턴스에 대해 미소유 참조를 하고 있습니다.
customer
가 미소유 참조이기 때문에 john
변수에 한 강한 참조를 사라지게 한 순간, Customer
에 대한 강한 참조는 더이상 존재않게 됩니다.
Customer
인스턴스에 대한 강한 참조가 더이상 존재하지 않게되어 인스턴스는 할당 해제 됩니다. 이 일이 일어난 뒤에, CreditCard
인스턴스에 대한 강한 참조 역시 더이상 존재하지 않기에 이 또한 할당 해제 됩니다.
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"
위에 있는 마지막 코드 조각은 john
변수가 nil
로 설정 된 후 Customer
인스턴스와 CreditCard
인스턴스가 둘 다 "디이니셜라이즈" 메시지를 출력하는 것을 보여주고 있습니다.
위의 약한 참조와 미소유 참조에 대한 예제는 일반적으로 강한 참조 순환을 부술 필요가 있는 시나리오중 2개를 보여주고 있습니다.
Person
과 Apartment
예제는 두 쪽의 속성이 nil
이 될 수도 있는 상황에서 강한 참조 순환의 가능성이 있는 상황 이었습니다. 이 시나리오는 약한 참조로 훌륭하게 해결 됩니다.
Customer
와 CreditCard
예제는 한 쪽의 속성이 nil
이 될 수 있고, 다른 쪽 속성은 nil
이 되지 않을때 강한 참조 순환이 생길 수 있는 상황 이었습니다. 이 시나리오는 미소유 참조로 훌륭하게 해결됩니다.
하지만 여기에 세번째 시나리오가 있습니다. 양 쪽의 속성이 모두 언제나 값을 가져야 하며, 속성은 초기화 완료 이후에 nil
이 되면 안되는 시나리오입니다. 이 시나리오에서는 한쪽 클래스의 미소유 속성과 다른 쪽 클래스의 암시적으로 드러난 옵셔널 속성이 유용합니다.
이는 양쪽의 속성이 초기화가 한번 완료된 이후에 옵셔널 속성을 드러낼 필요 없이 직접 접근이 될 수 있게 하며, 참조 순환이 일어나지 않게 합니다. 이번 섹션(section)은 이런 관계를 어떻게 설정하는지 보일 것입니다.
밑의 예제는 두개의 클래스 Country
와 City
를 정의합니다. 각각의 클래스는 서로의 클래스 인스턴스를 속성으로 저장합니다. 이 데이터 모델에서, 모든 나라들은 언제나 수도를 가지며, 모든 도시는 반드시 나라에 소속되어야합니다. 이를 표현하기 위해서 Country
는 capitalCity
속성을 가지고, City
클래스는 country
속성을 가집니다.
class Country {
let name: String
let capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
양 클래스 간의 상호 의존성을 설정하기 위해 City
이니셜라이저는 Country
인스턴스를 입력받고, 이 인스턴스를 country
속성에 저장합니다.
City
의 이니셜라이저는 Country
이니셜라이저 안에서 호출됩니다. 하지만 Country
이니셜라이저는 새 Country
인스턴스가 완전하게 이니셜라이즈 되기 전까지 self
를 City
로 넘길 수 없습니다. 이 단계 초기화 에서 설명된 것처럼 말이죠.
이 요구에 대처하기 위해 Country
의 capitalCity
속성을 암시적으로 드러난 옵셔널 속성으로 선언합니다. 그러기 위해서 타입 표시의 끝에 느낌표를 붙이면 됩니다(City!
). 이는 capitalCity
가 다른 옵셔널 값들처럼 nil
을 기본값으로 가짐을 뜻하지만, 암시적으로 드러난 옵셔널에서 설명한 것과 같이 접근하는데 드러내야할 필요가 없습니다.
capitalCity
가 기본값으로 nil
을 가지기에, 새 Country
인스턴스는 Country
인스턴스의 name
속성이 이니셜라이저 안에서 설정 되었을때를 완전히 완전히 초기화 된 순간이라고 간주합니다. 이는 Country
이니셜라이저가 암시적으로 name
속성이 설정 되는 순간부터 self
속성을 참조하고 넘겨줄 수 있다는 것을 뜻합니다. 그렇기에 Country
의 이니셜라이저는 자신의 capitalCity
속성을 설정할때 self
를 City
이니셜라이저의 파라메터로 넘겨줄 수 있습니다.
이 모든 것은 강한 참조 순환을 만들지 않고 Country
와 City
인스턴스를 한 문장(statement)안에서 만들 수 있다는 것을 뜻합니다. 그리고 느낌표를 통해 옵셔널 값을 드러내지 않고 capitalCity
속성에 직접 접근 할 수 있습니다.
var country = Country(name: "Canada", capitalName: "Ottawa")
println("\(country.name)'s capital city is called \(country.capitalCity.name)")
// prints "Canada's capital city is called Ottawa"
위의 예제에서는 암시적으로 드러난 옵셔널은 모든 두 단계의 클래스 이니셜라이저 요구사항이 모두 충족되었다는 것을 뜻합니다. capitalCity
속성에는 초기화 단계가 일단 끝나고 난후에는 옵셔널이 아닌 값처럼 접근이 가능합니다. 여전히 강한 참조 순환을 만들지 않으면서 말이죠.
위에서 어떻게 두 클래스 인스턴스의 속성들이 서로 강한 참조를 하면서 강한 참조 순환을 만드는지 보았습니다. 또한 약한 참조와 미소유 참조를 이용해 어떻게 강한 참조 순환을 부수는지도 보았습니다.
클로저를 클래스 인스턴스의 속성에 할당할때도 강한 참조 순환이 발생할 수 있습니다. 해당 클로저의 몸체는 인스턴스를 획득(capture)합니다. 이 획득은 클로저의 몸체가 self.someProperty
와 같은인스턴스의 속성에 접근하려 할 때 발생합니다. 혹은 클로저가 self.someMethod()
와 같은 인스턴스의 메소드를 호출 할 때도 발생합니다. 어느 경우든간에 그러한 접근에서 클로저는 self
를 획득하게 되며, 강한 참조 순환을 만들어냅니다.
이 강한 참조 순환은 클로저가 클래스와 같이 참조 타입 이기 떄문에 일어납니다. 클로저를 속성에 할당하면, 참조를 클로저에 할당하는 것이 됩니다. 본질적으로, 이는 위에서 말한 문제와 같은 문제입니다. 두개의 강한 참조가 서로를 살아있게 만듭니다. 하지만 이번엔 두개의 클래스 인스턴스가 아니라, 하나의 클래스 인스턴스와 클로저가 서로를 살아있게 합니다.
스위프트는 이 문제에 대해 클로저 획득 목록이라는 우아한 방법을 제공합니다. 하지만 클로저 획득 목록을 이용하여 강한 참조 순환을 부수는 방법을 배우기 전에, 어떻게 순환이 야기되는지 이해하는 것이 좋습니다.
밑의 예제는 self
를 클로저가 참조하면서 어떻게 강한 참조 순환이 생겨나는지 보여줍니다. 이 예제는HTMLElement
클래스를 정의해서 HTML 문서와 그 안에 포함된 개개의 요소를 모델링하고 있습니다.
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
println("\(name) is being deinitialized")
}
}
HTMLElement
클래스는 name
속성을 정의하여 요소의 이름을 가리키고 있습니다. 문단 요소인 "p"
나 줄바꿈 요소인 "br"
등. HTMLElement
는 또한 text
속성을 정의하여 HTML 속성내에서 텍스트가 설정되어서 보일 수 있게 합니다.
그런 간단한 두 속성 외에 HTMLElement
는 asHTML
이라는 느린(lazy) 속성을 정의합니다. 이 속성은 HTML 문자열 조각 안에 있는 name
과 text
조합된것을 참조합니다. asHTML
속성의 타입은 () -> String
이며, 다른 말로는 " 파라메터를 받지않고, String
값을 반환하는 함수" 라 할 수 있습니다.
기본적으로 asHTML
속성은 HTML태그의 문자열 표현을 반환하는 클로저에 할당되어있습니다. 이 태그는 옵셔널인 text
값이 존재할 경우 그것을 포함하게 되며, text
가 존재하지 않을때는 아무런 텍스트 내용을 가지지 않습니다. 문단 요소에 대해 이 클로저는 text
속성이 "some text"
나 nil
중 어느것에 해당하는지에 따라서 <p>some text</p>
를 반환하거나 <p />
를 반환합니다.
이 asHTML
은 인스턴스 메소드와 비슷한 것처럼 이름 지어지고, 사용됩니다. 하지만 asHTML
은 인스턴스 메소드가 아닌 클로저 속성이기에, 특정 HTML 요소에 대해 HTML 렌더링을 바꾸고 싶다면 기본값을 대체하여 맞춤(custom) 클로저로 바꿀 수 있습니다.
NOTE 이
asHTML
속성은 느린(lazy) 속성으로 선언되어 있습니다. 특정 HTML 출력 목표에 대해 문자열 값을 렌더링해야할 필요가 있을때만 필요해지기 때문입니다.asHTML
이 느림 속성이기 때문에self
를 기본 클로저 안에서 참조할 수 있습니다. 느린 속성은 초기화가 완료 되어self
가 존재하기 전까지는 접근이 되지 않기 때문입니다.
HTMLElement
클래스는 하나의 이니셜라이저를 제공하여 name
인자와 필요하다면 text
인자를 받아 새 요소를 초기화합니다. 또한 이 클래스는 디이니셜라이저를 정의하여 HTMLElement
가 할당 해제 될 때 메시지를 출력하게 합니다.
여기 HTMLElement
클래스를 생성하여 새 인스턴스가 어떻게 출력을 하는지 예제가 있습니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints "<p>hello, world</p>"
NOTE 위의
paragraph
변수는 옵셔널HTMLElement
로 정의되어있습니다. 그래서 아래에서nil
로 설정되면 강한 참조 순환이 존재하게 됨을 보일 수 있습니다.
안타깝게도, 위에 쓰여진대로 HTMLElement
클래스는 HTMLElement
인스턴스와 asHTML
의 기본값으로 설정된 클로저 사이에 강한 참조 순환을 만들게 되었습니다. 그림은 그 순환이 어떻게 생겼는지 보여줍니다.
인스턴스의 asHTML
속성은 해당 클로저에 대해 강한 참조를 하고 있습니다. 하지만 클로저가 그 몸체 안에서 self.name
과 self.text
를 참조하는 방법으로 self
를 참조하고 있기에 클로저는 인스턴스 자신을(slef) 획득하게 됩니다. 즉 HTMLElement
인스턴스를 참조하게 되어 강한 참조를 하게 됩니다. 이렇게 둘 사이에 강한 참조 순환이 형성되게 됩니다. (클로저의 값 획득에 대해서 더 자세한 정보는 값 획득을 보세요)
NOTE 클로저가
self
를 여러번 참조한다고 해도,HTMLElement
에 대한 강한 참조는 오직 한번만 획득하게 됩니다.
만약 paragraph
변수를 nil
로 설정하고, 이 HTMLElement
에 대한 강한 참조를 부순다면, HTMLElement
인스턴스나 그 클로저는 할당 해제되지 않습니다. 강한 참조 순환이 있기 때문입니다.
paragraph = nil
HTMLElement
의 디이니셜라이저가 아무런 메시지도 출력하지 않았음에 주의하세요. 이는 곧 HTMLElement
인스턴스가 할당 해제 되지 않았음을 의미합니다.
클로저와 클래스의 강한 참조 순환은 클로저 정의의 일부로서 획득 목록을 정의하는 것으로 해결 할 수 있습니다. 획득 목록은 하나 이상의 참조 타입이 클로저의 몸체에 있을때 사용할 규칙을 정의합니다. 두 클래스 인스턴스 간의 강한 참조 순환 처럼, 획득한 참조를 강한 참조대신 약한 참조나 미소유 참조로 선언할 수 있습니다. 약한 참조나 미소유 참조중 어느 것이 더 적절한지는 코드의 다른 부분에 따라 다릅니다.
NOTE 스위프트는
self
의 멤버를 클로저 안에서 참조할때someProperty
나someMethod
대신에self.someProperty
나self.someMethod
로 표기할 것을 요구합니다. 이렇게 함으로써self
가 의도치 않게 획득 될 수 있음을 기억하는데 도움이 됩니다.
획득 목록의 각각의 아이템은 self
나 someInstance
같은 클래스 인스턴스와 참조간의 약한 참조 또는 미소유 참조의 쌍입니다. 각 쌍들은 대괄호안에 쓰여지며, 콤마로 구분됩니다.
획득 목록은 클로저에 파라메터 목록이나 반환 타입이 있다면 그 앞에 위치시킵니다.
@lazy var someClosure: (Int, String) -> String = {
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
만약 클로저의 파라메터 목록이나 반환 타입이 문맥에서 암시되어 특별히 정해지지 않았다면, 획득 목록은 클로저의 시작 부분인 in
바로 앞에 둡니다.
@lazy var someClosure: () -> String = {
[unowned self] in
// closure body goes here
}
클로저와 클로저가 획득한 인스턴스가 언제나 서로를 참조할때, 획득을 미소유 참조로 정의합니다. 그렇게 되면 같은 시점에 서로 할당 해제가 될것입니다.
그와 반대로, 클로저가 획득한 참조가 언젠가는 nil
이 될때, 획득을 약한 참조로 정의합니다. 약한 참조는 언제나 옵셔널 타입이며, 참조중인 인스턴스가 할당 해제가 되면 자동적으로 nil
이 됩니다. 이로 인해 클로저 몸체에서 인스턴스의 존재를 확인할 수 있습니다.
NOTE 만약 획득된 참조가 절대로
nil
이 되지 않는다면, 그 참조는 약한 참조보다 미소유 참조로 해야할것입니다.
미소유 참조는 위의 HTMLElement
예제에서 강한 참조 순환을 풀기에 적절한 획득 방법입니다. 다음은 HTMLElement
클래스가 순환을 어떻게 피해야 할지 보여줍니다.
class HTMLElement {
let name: String
let text: String?
@lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
println("\(name) is being deinitialized")
}
}
위의 HTMLElement
구현은 이전의 구현과 동일합니다. asHTML
클로저의 획득 목록 부분을 제외하면 말이죠. 이 경우에 획득 목록은 [unowned self],
며, 이는 "인스턴스를 강한 참조가 아닌 미소유 참조로서 획득한다"라 할 수 있습니다.
이제 이전처럼 HTMLElement
인스턴스를 생성하여 출력할 수 있습니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
println(paragraph!.asHTML())
// prints "<p>hello, world</p>"
이 그림은 획득 목록을 사용한 참조들이 어떻게 보여줍니다.
이번의 클로저에 의한 self
획득은 미소유 참조입니다. 그렇기에 획득한 HTMLElement
인스턴스를 강하게 유지하지 않습니다. 만약 paragraph
변수의 강한 참조를 nil
로 설정한다면, HTMLElement
인스턴스는 할당 해제가 될 것입니다. 밑의 예제에서 보이는 것처럼 디이니셜라이저 메시지를 출력하면서 말이죠.
paragraph = nil
// prints "p is being deinitialized"
Translator : 허혁 (hyukhur@gmail.com)
선택 묶임(Optional chaining)란 nil이 될 수 있는 선택지(options)를 가진 프로퍼티(property), 메소드(method), 서브 스크립트 (subscript)에 질의하고 호출하는 프로세스를 말한다. 만약 어떤 선택지가 값을 가진다면 프로퍼티, 메소드, 서브스크립트 호출은 성공하고 선택지가 nil이면, 프로퍼티, 메소드, 서브스크립트 호출은 nil을 반환하게 된다. 여러개의 질의도 함께 엮일 수 있으며, 만약 묶임(chaining) 중간의 어떤 링크가 nil이라면 조용하게 전체 체인은 실패한다.
노트 스위프트(Swift)의 선택 묶임가 오브젝티브씨(Objective-C)에 있는 nil에 메시지 보내기와 유사하다. 그러나, 모든 타입(any type)에서 동작하고, 성공, 실패 여부를 확인할 수 있다는 점에서 차이가 있다.
호출하고자 하는 프로퍼티, 메소드, 서브스크립트의 선택지 값(optional value)이 nil 아닐 때 선택지 값 뒤에 물음표(?)를 두어 선택 묶임를 둘 수 있다. 이것은 선택지 값 뒤에 느낌표(!)를 두어 그 값을 강제로 랩핑 해제하는 것과 유사하다. 가장 주요한 차이점은 선택 묶임는 선택지가 nil일 때 자연스럽게 실패한다는 것이고, 강제 랩핑 해제는 선택지가 nil인 경우 런타임 에러가 발생한다. 선택 묶임가 nil 값에도 호출할 수 있다는 사실을 반영하기 위해 선택 묶임 호출 결과는 항상 선택지 값이다. 비록 질의한 프로퍼티, 메소드, 서브스크립트가 항상 선택지 값이 아닌 결과를 도출해도 그렇다. 이 선택지 반환 값을 사용해서 선택 묶임 호출이 성공했는지 ( 반환된 선택지가 값을 가지는 ) 묶임 중간의 nil 값 ( 선택지 반환값이 nil ) 때문에 실패했는지를 확인할 수 있다. 구체적으로, 선택 묶임 호출 결과는 선택지로 감싸여져 있음에도 기대한 반환값과 동일한 타입이다. 일반적으로 Int를 반환하는 프로퍼티는 선택 묶임에 따라 접근이 가능할때는 Int?를 반환할 것이다. 다은 몇몇 코드 조각은 선택 묶임가 어떻게 강제 랩핑 해제와 다르고 성공 여부 확인을 가능케 하는지 보여준다. 먼저 Person과 Residence 라는 클래스를 정의하자.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}]
Residence 인스턴스(Instance)는 기본값이 1인 numberOfRooms 이라는 단 하나의 Int 프로퍼티를 가진다. Person 인스턴스는 Residence? 타입으로 residence 이라는 선택적 프로퍼티를 가진다. 만약 Person 이라는 인스턴스를 새로 만들면, 선택지가 된 효과에 따라 기본적으로 nil로 설정된다. 아래 코드에서는 john는 nil로 된 residence 프로퍼티를 가질 것이다. let jone = Person() 만약 Person의 residence의 numberOfRooms 프로퍼티를 그 값을 강제로 랩핑 해제를 하려고 느낌표를 붙여서 접근한다면 런타임 에러(Runtime Error)를 유발시킬 것이다. 왜냐하면 해제할 residence 값 자체가 없기 때문이다. let roomCount = john.residence.numberOfRooms 위 코드는 john.residence가 nil이 아닌 값을 성공하며 방 갯수에 적절한 숫자를 담고 있는 Int 값에 roomCount를 설정할 것이다. 그러나 이 코드는 위에 보여지는 것처럼 residence가 nil이라면 항상 런타임 에러를 유발 시킨다. 선택 묶임는 numberOfRooms 값에 접근하는데 대안법을 제공한다. 선택 묶임를 사용하기 위해 느낌표 자리에 물음표를 사용하면 된다.
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// prints "Unable to retrieve the number of rooms."
이것은 스위프트(swift)가 선택적 residence 프로퍼티를 "엮고" 만약 residence가 있으면 numberOfRooms 값을 가져온다는 것을 말해준다.
numberOfRooms에 대한 접근이가 잠제적으로 실패할 수 있기 때문에 선택 묶임는 Int?이나 "선택적 Int"형 값을 반환하려고 한다. 위 예제처럼 residence가 nil인 경우는 numberOfRooms에 대한 접근이 불가능하다는 사실을 반영하기 위해서 이 선택적 Int 역시 nil이 될 것이다. numberOfRooms가 비선택적 Int 임에도 불구하고 참인 것을 명심해라. 선택 묶임를 통해 질의한다는 것은 numberOfRooms가 Int 대신 Int?를 항상 반환할 것이라는 것을 의미한다. john.residence에 Residence 인스턴스를 할당할 수 있는데 그러면 더이상 nil 값은 존재하지 않게 된다.
john.residence = Residence()
john.residence는 실체 Residence 인스턴스를 이제 가지게 되었다. 만약 예전과 동일한 선택 묶임를 사용해 접근하려고 하면, 1이라는 numberOfRooms 기본값을 가지는 Int?가 반환될 것이다.
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// prints "John's residence has 1 room(s)."
프로퍼티, 메소드, 서브스크립트를 호출하는 것 같은 한단계 더 깊은 선택 묶임을 사용할 수 있다. 이는 상호관계있는 타입간의 복잡한 모델에서 서브 프로퍼티(subproperty)를 파고 들 수 있게 해주고 그 서브 프로터티에 프로퍼티와 메소드, 서브스크립트에 접근할 수 있는지 아닌지를 확인할 수 있게 해준다. 다음 코드 조각은 다단계 선택 묶임 예를 포함한 몇가지 순차적인 예제에서 사용될 4개의 모델 클래스를 정의한다. 이 클래스들은 위에 나온 Person과 Residence 모델에 Room과 Address 클래스를 추가하고 연관 프로퍼티와 메소드, 서브스크립트를 확장한다. Person 클래스는 이전과 동일한 방법으로 정의한다.
class Person {
var residence: Residence?
}
Residence 클래스는 이전보다 조금 복잡해졌다. 이번에는 Residence 클래스에 Room[] 타입의 빈 배열로 초기화된 rooms라는 변수 프로퍼티를 선언한다.
class Residence {
var rooms = Room[]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
println("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
이번 버전 Residence는 Room 인스턴스 배열을 저장하기 때문에, 그 numberOfRooms 프로퍼티는 저장된 프로퍼티가 아닌 계산된 프로퍼티로 구현했다. 계산된 numberOfRooms 프로퍼티는 단순히 rooms 배열에서 count 프로퍼티의 값을 반환한다. 그 rooms 배열에 접근하기 위한 바로가기로 이번 버전 Residence는 읽기만 가능한 서브 스크립트를 제공하는데 서브스크립트에게 전달받는 인덱스(index)가 적합할 것이라는 가정으로 시작해보겠다. 만약 인덱스가 적합하다면, 서브스크립트는 rooms 배열의 요청받은 인덱스의 방정보를 반환할 것이다. 또한 이번 버전 Residence는 printNumberOfRooms라는 이름의 메소드를 제공하는데 단순히 Residence에 방 갯수를 출력한다. 마지막으로 Residence에 Address?이란 타입으로 address라는 선택적 프로퍼티를 선언한다. 이를 위한 Address 클래스 타입은 밑에 정의하겠다. rooms 배열에 사용하는 Room 클래스는 name이라는 프로퍼티 하나를 가지는 간단한 클래스인데 이는 적절한 방이름을 설정하기 위한 초기화 역할(initializer)을 한다.
class Room {
let name: String
init(name: String) { self.name = name }
}
이 모델의 마지막 클래스는 Address이다. 이 클래스는 String? 타입의 선택적 프로퍼티를 3개 가지고 있다. 그 중 2개는 buildingName과 buildingNumber 인데 주소를 구성하는 특정 빌딩에 대한 구분을 짓기 위한 대체 수단이다. 3번째 프로퍼티인 street는 그 주소의 도로이름에 사용한다.
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName {
return buildingName
} else if buildingNumber {
return buildingNumber
} else {
return nil
}
}
}
또한 Address 클래스는 String? 반환값을 가지는 buildingIdentifer 이란 이름의 메소드를 제공한다. 이 메소드는 buildingName과 buildingNumber 프로퍼티를 확인해서 만약 buildingName이 값을 가진다면 그 값을 혹은 buildingNumber이 값을 가진다면 그 값을, 둘다 값이 없다면 nil을 반환한다.
강제 랩핑 해제(Forced Unwrapping) 대안으로써 선택 묶임에서 봤던 것처럼 선택 묶임를 선택적 값에 대한 프로퍼티 접근에 접근할 수 있는지 만약 프로퍼티 접근이 가능한지 확인하기 위해 사용할 수 있다. 그러나선택 묶임를 통해 프로퍼티의 값을 설정하는 것은 할 수 없다. 위에 정의한 새로운 Person 인스턴스를 사용해 클래스를 만들어 이전처럼 numberOfRooms 프로퍼티에 접근하기를 시도해본다.
let john = Person()
if let roomCount = john.residence?.numberOfRooms {
println("John's residence has \(roomCount) room(s).")
} else {
println("Unable to retrieve the number of rooms.")
}
// prints "Unable to retrieve the number of rooms."
john.residence가 nil이기 때문에 이 선택 묶임를 예전과 동일한 방식으로 호출했지만 에러 없이 실패한다.
선택 묶임를 사용해서 선택적 값을 호출하고 메소드 호출이 성공했는지 여부를 확인해볼 수 있다. 설렁 메소드가 반환값을 정의하지 않더라고 할 수 있다. Residence 클래스에 있는 printNumberOfRooms 메소드는 numberOfRooms의 현재 값을 출력한다. 그 메소드는 다음과 같을 것이다.
func printNumberOfRooms() {
println("The number of rooms is \(numberOfRooms)")
}
이 메소드는 반환값을 명시하지 않았다. 그러나 반환형이 없는 함수와 메소드는 Functions Without Return Values에 나와 있는 것처럼 암시적으로 Void 타입을 반환하게 된다. 만약 선택 묶임에 있는 선택지 값에 이 메소드를 호출한다면, 메소드 반환형은 Void가 아니라 Void?이 될 것이다. 선택 묶임를 통해 호출될 때 선택적 타입은 항상 반환 값을 가지기 때문이다. 이는 메소드가 반환값이 정의되어 있지 않더라도 printNumberOfRooms 메소드를 호출이 가능한지를 if문을 써서 확인할 수 있게 한다. printNumberOfRooms에서 암시적 반환값은 만약 메소드가 선택 묶임를 통해 성공적으로 호출되었다면 Void와 동일할 것이고 그렇지 않다면 nil과 동일할 것이다.
if john.residence?.printNumberOfRooms() {
println("It was possible to print the number of rooms.")
} else {
println("It was not possible to print the number of rooms.")
}
// prints "It was not possible to print the number of rooms."
선택적값에 대한 서브스크립트에서 값을 가져와서 서브스크립트 호출이 성공했는지 확인하기 위해 선택 묶임를 사용할 수 있다. 그러나 선택 묶임를 통해 서브스크립트로 값을 설정하는 것은 할 수 없다.
노트 선택연쇄를 통해 선택적값에 대한 서브스크립트를 접근할 때 서브스크립트 꺽은 괄호(bracket) 앞에 물음표를 놓아야 한다. 뒤가 아니다. 선택연쇄 물음표는 항상 선택적인 표현식의 뒤에 바로 따라나와야 한다.
아래 예는 Residence 클래스에 정의되어 있는 서브스크립트를 사용하는 john.residence 프로퍼티의 rooms 배열에 있는 첫번째 방이름을 집어오려고 하는 것이다. john.residence가 현재 nil이기 때문에 서브스크립트는 실패한다.
if let firstRoomName = john.residence?[0].name {
println("The first room name is \(firstRoomName).")
} else {
println("Unable to retrieve the first room name.")
}
// prints "Unable to retrieve the first room name."
이 서브스크립트 호출 속에 있는 선택연쇄 물음표는 john.residence 바로 뒤, 서브스크립트 꺽은 괄호 전에 존재해야한다. 왜냐하면, john.residence가 선택연쇄를 꾀할 선택적 값이기 때문이다. 만약, john.residence에 rooms 배열에 한개 이상의 Room 인스턴스도 같이 실제 Residence를 만들어서 할당한다면 선택 묶임를 통해 rooms 배열안의 실제 아이템에 접근하기 위해서 Residence 서브스크립트를 사용할 수 있다.
let johnsHouse = Residence()
johnsHouse.rooms += Room(name: "Living Room")
johnsHouse.rooms += Room(name: "Kitchen")
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
println("The first room name is \(firstRoomName).")
} else {
println("Unable to retrieve the first room name.")
}
// prints "The first room name is Living Room."
프로퍼티와 메소드, 서브스크립트를 사용해 모델 깊이 파고들기 위해서 선택 묶임를 여러 단계로 함께 엮을 수 있다. 그러나 다단계 선택 묶임로 반환값에 더 많은 선택적임 단계를 넣을 수는 없다. 다른 방식으로:
if let johnsStreet = john.residence?.address?.street {
println("John's street name is \(johnsStreet).")
} else {
println("Unable to retrieve the address.")
}
// prints "Unable to retrieve the address."
john.residence의 값은 현재 적합한 Residence 인스턴스를 포함하고 있다. 그러나 john.residence.address의 값은 현재 nil이다. 이때문에, john.residence?.address?.street 호출은 실패한다.
위 예제를 잘 생각해보자. street 프로퍼티 값을 집어오고자 했다. 이 프로퍼티는 String? 이다. 그러므로 john.residence?.address?.street 의 반환값 역시 두단계 선택 묶임로 프로퍼티가 선택적 타입에 추가로 더해 적용되었음에도 불구하고 String? 이다.
만약 john.residence.address 의 값으로써 실제 Address 인스턴스를 설정하고 그 Adress의 street 프로퍼티에 실제 값을 설정한다면, 다단계 선택 묶임를 통해 그 프로퍼티 값을 접근할 수 있을 것이다.let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence!.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
println("John's street name is \(johnsStreet).")
} else {
println("Unable to retrieve the address.")
}
// prints "John's street name is Laurel Street."
john.residence.address 의 address 인스턴스에 할당하기 위해서 느낌표를 사용한 것을 잘보자. john.residence 프로퍼티는 선택적 타입을 가지기에 Residence의 address 프로퍼티에 접근하기 전에 느낌표를 사용해서 그 실제 값을 까볼 필요가 있다.이전 예제는 선택 묶임를 사용해서 선택적 타입의 프로퍼티의 값을 어떻게 집어오는지 보여주었다. 또한 선택 묶임을 사용해서 선택적 타입 값을 반환하는 메소드를 호출하고 필요하다면 그 메소드의 반환값을 연결할 수 있었다. 아래 예제는 선택 묶임을 통해 Address 클래스의 buildingIndentifer 메소드를 호출한다. 이 메소드는 String? 타입의 값을 반환한다. 이전에 설명한데로, 선택 묶임에 따라 호출된 메소드의 최종 반환값 또한 String?이 된다.
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
println("John's building identifier is \(buildingIdentifier).")
}
// prints "John's building identifier is The Larches."
만약 이 메소드 반환값 이상의 선택 묶임을 실행하기 원한다면, 메소드 둥근 괄호(parentheses) 다음에 선택 묶음 물음표를 두면 된다.
if let upper = john.residence?.address?.buildingIdentifier()?.uppercaseString {
println("John's uppercase building identifier is \(upper).")
}
// prints "John's uppercase building identifier is THE LARCHES."
노트 위 예제에서 둥근 괄호 다음에 선택 묶음 물음표를 놓았는데, 묶고자 하는 선택적 값이 buildingIndentifer 자체가 아니라 buildingIndentifer 메소드의 반환값이기 때문이다.
Translator : Snowcat8436 (snowcat8436@gmail.com)
타입 변환이란 인스턴스(instance)의 타입을 체크하기 위한 방법이며, 또한 이것은 인스턴스를 마치 해당 클래스가 가친 계층구조에서 온 상위클래스나 하위클래스처럼 다룬다.
Swift에서 타입 변환은 is
와 as
라는 연산자로 구현할 수 있으며, 이 두 연산자는 값의 타입을 체크하거나 다른 타입으로 변환하는 간단하고 표현적인 방법을 제공합니다.
또한 해당 타입이 프로토콜에 적합하지 아닌지 체크하기 위해서 타입 변환을 사용할 수 있으며 보다 자세한 사항은 Protocol Conformance를 참조하시기 바랍니다.
당신은 특정한 클래스의 인스턴스의 타입을 체크하거나 인스턴스를 같은 계층의 또다른 클래스로 변환하기 위해서 클래스들과 하위클래스들의 계층정보를 사용한 타입캐스팅을 할 수 있다. 아래의 세가지의 코드조각(code snippets)는 타입 캐스팅이 사용되는 예제를 보여주기 위한 계층적인 클래스들과 각각의 클래스들의 인스턴스를 포함하는 배열(array)를 정의하고 있습니다. 첫번째 코드 조각은 MediaItem이라는 새로운 기본 클래스(base class)를 정의하고 있습니다. 이 클래스는 디지털 미디어 라이브러리에 있는 모든 아이템들을 위한 기본적인 기능을 제공합니다. 특히 문자열(String)타입의 name 속성(Property)를 선언하고, initializer를 통해서 'name'을 초기화 합니다(이것은 모든 미디어 아이템(영화나 노래)들이 이름을 가지고 있다고 가정합니다)
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
다음 코드 조각은 MediaItem의 두가지 하위클래스(subclasses)들입니다. 첫번째 하위클래스인 Moive는 내부적으로 영화에 관한 추가적인 데이터를 가지고 있는데. 이는 'director'라는 속성 및 초기화 부분을 MediaItem클래스의 initalizer의 윗부분에 추가하는것으로 더할 수 있으며. 두번째 하위 클래스인 'Song'도 artist 속성의 관한 내용을 선언하고 base class의 윗부분에서 이를 초기화 한다:
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
마지막 코드조각은 2개의 Movie 인스탄스와 3개의 Song 인스턴스를 포함하는 library로 불리는 상수형 배열(constant array)를 만든다. library배열의 타입은 각각의 배열 내부의 콘텐츠를 초기화 하는 타입으로 추정할 수 있다. Swift의 타입 체커는 Movie나 Song이 공통의 상위 클래스(superclass)인 MediaItem을 가진다고 추정할 수 있고, 따라서 library의 타입은 MediaItem[]로 추정할 수 있다 :
let library = [
Movie(name: "Casablanca", director: "Michael Curtiz"),
Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
Movie(name: "Citizen Kane", director: "Orson Welles"),
Song(name: "The One And Only", artist: "Chesney Hawkes"),
Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// the type of "library" is inferred to be MediaItem[]
library에 저장된 요소들은 해당 Scenes뒤에서는 여전히 Movie와 Song인스탄스이다. 그러나 만일 네가 이 array의 컨텐츠들을 반복자 등을 이용하여 뽑아낸 다면, 네가 받게된 그 아이템들의 타입은 Song이나 Movie이 아닌 MediaItem일 것이다. 그것들을 원래의 타입으로 작업을 하기 위해얻고 싶다면, 당신은 그들의 타입을 체크하는 것이 필요하고, 또한 그들을 다운캐스트해서 다른 타입으로 변경하여야 한다. 이는 아래서 설명하도록 하겠다.
어떠한 인스턴스가 확실히 하위클래스 타입인지 아닌지를 체크하기 위해서는 타입 체크 연산자인 is
를 이용합니다. 이 타입체크용 연산자는 만일 해당 인스탄스가 해당 하위 클래스라면 true
를, 아니라면 false
를 반환합니다.
아래의 예시는 library배열에 있는 Movie의 인스턴스의 수와 Song의 인스턴스의 수를 세기 위한movieCount와 songCount라는 두개의 변수를 선언하는 것을 보여줍니다.:
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie {
++movieCount
} else if item is Song {
++songCount
}
("Media library contains \(movieCount) movies and \(songCount) songs")
// prints "Media library contains 2 movies and 3 songs"
이 예시에서는 library배열의 모든 아이템에 대해서 작업하며, 각각의 1번의 과정마다 for-in loop는 배열에 MediaItem 상수를 가져오고, 각각의 아이템이 만일 Movie 인스탄스이면 is Movie에서 true를 아니라면 false를 반환하고, 이와 유시하게 아이템이 만일 Song의 인스탄스인지 아닌지에 따라 is Song 체크부분의 리턴값이 결정됩니다. for-in loop의 마지막이 되면, moveCount와 songCount의 값을 보고 전체 MediaItem인스탄스중에 각각의 타입이 얼마만큼의 수가 들어있는지 찾아낼 수 있다.
상수와 변수의 명확한 클래스 타입은 사실은 아마도 the scenes뒤에 있는 하위 클래스의 인스탄스에 속할것이다. 당신이 위와 같은 케이스를 믿는 경우, 당신은 타입 변환 연산자인 'as'를 통하여 하위클래스타입으로 다운캐스팅을 시도 할 수 있다. 다운캐스팅은 실패할 수 있기때문에, 타입 캐스팅연산자는 두가지의 다른 형태를 가집니다. 하나는 as?와 같은 연산자를 사용하는 optional form으로 다운캐스팅을 시도하여 optional value를 리턴합니다. 다른 하나는 as와 같은 연산자를 사용하는 forced form으로 다운 캐스팅을 시도하고 강제로 unwrap한 결과를 한번에 합한 작업을 합니다. 네가 만일 다운캐스트가 성공할지 확신을 가지지 못한다면 타입변환 연산자인 as?를 이용하는 optional form을 사용한다. 위 연산자를 사용하는 form은 항상 optional value를 리턴하며 그래서 만일 다운 캐스트가 가능하지 않은 경우에는 nil을 리턴할 수 있도록 할 수 있다. 이 것은 당신이 다운 캐스팅의 성공 유무를 체크할 수 있도록 하게 한다. 오직 당신이 다운캐스트가 항상 성공할 것이라는 확신이 있다면 타입 변환 연산자인 as를 이용하는 forced form을 사용할 수 있다. 위의 연산자를 사용하는 form은 만일 올바르지 않은 클래스 타입으로 다운캐스팅을 시도했을 시에 런타임 에러를 발생시킨다. 아래에 library 내의 각 MediaItem을 반복해가면서 각 아이템들을 위한 적절한 설명을 출력하는 예시를 만들었다. 이를 위해서 각 아이템이 단순히 MediaItme이 아닌 진정으로 Movie나 Song인지억세스 해볼 필요가 있다. 이를 위해서설명을 출력하기 위해서 Movie나 Song의 director나 artist 속성에 접근할수 있게 할 필요가 있다. 예시에서 배열내의 각각의 item은 Movie이거나 Song이라고 생각된다. 당신은 각각의 아이템이 실제 어떠한 클래스인지 미리 알 수가 없습니다. 그러므로 optional form을 위한 as? 연산자를 사용하여 루프를 통해 각 케이스마다 다운캐스팅을 체크하는 것이 적절합니다:
for item in library {
if let movie = item as? Movie {
println("Movie: '\(movie.name)', dir. \(movie.director)")
} else if let song = item as? Song {
println("Song: '\(song.name)', by \(song.artist)")
}
}
// Movie: 'Casablanca', dir. Michael Curtiz
// Song: 'Blue Suede Shoes', by Elvis Presley
// Movie: 'Citizen Kane', dir. Orson Welles
// Song: 'The One And Only', by Chesney Hawkes
// Song: 'Never Gonna Give You Up', by Rick Astley
이 예시는 현재 아이템이 Movie라고 생각하고 다운 캐스팅을 시도하는 것으로 시작합니다. 아이템이 MediaItem 인스탄스이므로 이 아이템은 Movie일 수 있습니다, 또한 똑같은 이유로 Song도 가능합니다, 혹은 오로지 단순히 MediaItem일수도 있습니다. 이것이 불확실 하기 때문에, as?
타입변환 연산자를 사용하여 하위 클래스로의 다운캐스팅을 시도시에 optional value를 반환합니다. 그 결과 item as Moive의 결과는 Move? 타입, 즉 optional Movie이 됩니다.
library 배열안의 두개의 Song 인스탄스에 해당 내용을 적용하여 Movie로 다운캐스팅을 할경우 실패한다. 이것에 대처하기 위해, 위의 예시에서는 결과로 나온 optional Movie값이 실제로 값을 가지고 있는지 체크하기 위한(이 경우는 다운캐스팅이 성공했는지 아닌지 찾는 과정이다) optional binding을 사용한다.
이 optional binding 은 "if let movie = is as? Moive"와 같이 적히며, 이는 다음과 같이 해석될 수 있다: "해당 아이템을 Movie로 생각하고 접근을 시도한다. 만일 해당 작업이 상공하면, 반환된 optional Movie값을 저장할 movie라고 불리는 새로운 임시 상수값을 설정한다.
만일 다운 캐스팅이 성공한다면, movie의 속성들을 가지고 director와 같은 Moive 인스탄스를 위한 설명을 출력하는데 사용할 수 있습니다.
비슷한 원리로 Song 인스턴스를 위한 체크를 하여, library에서 Song인스탄스를 찾기만 한다면, artist와 같은 적절한 설명을 출력할 수 있습니다.
NOTE 변환(Casting)은 실제로 해당 인스턴스를 수정하거나 그 값을 바꾸는 것이 아닙니다. 근본적인 인스턴스는 처음상태 그대로 남아있습니다. 이것은 간단히 특별한 것이며, 캐스팅된 타입의 인스턴스로서 접근이 가능한 것입니다.
Swift는 특정한 타입을 가지지 않는 상태로 작업하기 위한 두가지의 특별한 타입을 제공합니다:
AnyObject
는 어떠한 클래스타입의 인스턴스라도 표현할 수 있습니다
Any
는 함수형의 타입을 제외하고는 어떠한 타입의 인스턴스라도 표현할 수 있습니다.
NOTE
Any
나AnyObject
는 오로지 당신이 명시적으로 behavior나 그들이 제공하는 능력들이 필요한 경우에만 사용합니다. 이는 항상 당신의 코드 속에서당신이 예상한 특정한 형태의 타입으로 작동하는 것이 더 낫습니다.
Cocoa APIS를 이용하여 작업을 할때, 보통 AnyObject
[] 타입의 배열을(AnyObject
타입의 값을 가진 배열) 받는것이 일반적입니다. 이것은 Objective-C가 명시적인 타입의 배열을 가지지 못하기 때문입니다. 그러나 당신이 종종 당신이 알고있는 API가 제공하는 배열을 포함한 여러가지 정보를 포함한 오브젝트들의 타입에 대해서 자신이 있을 수 있다.
이러한 상황에서, 당신은 optional unwrapping이 필요하지 않은 경우에 한하여 배열의 각각의 아이템을 특정한 클래스의 타입으로 바꾸는 다운캐스팅을하기 위한 타입 변환 연산자 as
로 강제로 변경한 형태를 사용할 수 있습니다.
let someObjects: AnyObject[] = [
Movie(name: "2001: A Space Odyssey", director: "Stanley Kubrick"),
Movie(name: "Moon", director: "Duncan Jones"),
Movie(name: "Alien", director: "Ridley Scott")
]
이 배열은 오로지 Moive 인스턴스만 가지는 것을 이미 알고 있으므로, 당신은 다운캐스팅 및 타입 변환 연산자 as
를 이용하여 non-optional Moive로 강제로 형태를 바꾸는 unwrap를 할 수 있습니다.
for object in someObjects {
let movie = object as Movie
println("Movie: '\(movie.name)', dir. \(movie.director)")
}
// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones
// Movie: 'Alien', dir. Ridley Scott
루프를 조금더 짧게 만들기 위해서, 각 아이템을 다운캐스팅하는 대신에 someObjects 배열을 Movie[]타입으로 다운 캐스팅 할 수도 있습니다:
for movie in someObjects as Movie[] {
println("Movie: '\(movie.name)', dir. \(movie.director)")
}
// Movie: '2001: A Space Odyssey', dir. Stanley Kubrick
// Movie: 'Moon', dir. Duncan Jones// Movie: 'Alien', dir. Ridley Scott
이곳에 non-class타입을 포함한 여러가지 다른 타입을 섞어서 작업하기 위한 Any를 사용한 예제가 있다. 이 예제는 Any
타입의 값을 저장할 수 있는 thing이라는 한 배열을 생성한다.
var things = Any[]()
things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
thing배열은 두개의 int
값, 두개의 Double
값, 하나의 String
값, 하나의 (Double
,Double
)타입의 tuple, 그리고 "Ghostbusters"의 name과 "Ivan Retiman"의 director속성을 가진 Moive를 한개 가지고 있다.
당신은 Any
나 AnyObject
로 알고있는 변수에서 특정한 타입의 상수나 변수 찾기 위한 스위치 구문의 case
항목에 is
와 as
연산자를 사용할수 있습니다.
아래의 예제는 아이템들의 things 배열의 각 아이템을 반복하면서 스위치 문을 통해서 각각의 타입을 요청한다.
몇몇의 switch
문의 case
항목에서 비교항목과 동일한 값과 타입을 가지는 상수의 경우는 해당 값과 형태를 출력한다:
for thing in things {
switch thing {
case 0 as Int:
println("zero as an Int")
case 0 as Double:
println("zero as a Double")
case let someInt as Int:
println("an integer value of \(someInt)")
case let someDouble as Double where someDouble > 0:
println("a positive double value of \(someDouble)")
case is Double:
println("some other double value that I don't want to print")
case let someString as String:
println("a string value of \"\(someString)\"")
case let (x, y) as (Double, Double):
println("an (x, y) point at \(x), \(y)")
case let movie as Movie:
println("a movie called '\(movie.name)', dir. \(movie.director)")
default:
println("something else")
}
}
// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called 'Ghostbusters', dir. Ivan Reitman
NOTE
switch
문의case
항목들은 체크 및 특정한 타입으로의 변환을 위해서as
나as?
를 통해서 강제로 변경된 형태를 사용한다. 이런 체크는switch
문의 문맥안에 있는 이상 항상 안전하다.
Translator : Alice Kim (mail@jua.kim) 번역에 대한 의견이 있을 때에는 메일이나 Chapter21 - Discussion 에 의견을 남겨주세요
열거형(Enumerations)은 종종 특정 클래스 또는 구조체(structure)의 기능을 지원하기 위해 만들어집니다. 마찬가지로, 복잡한 형태의 맥락에서 사용하기위한 유틸리티 클래스 또는 구조체를 정의하는데 유용합니다. 이를 위해 Swift는 중첩을 지원하는타입의 정의 안에 열거형, 클래스, 구조체를 내장타입으로 사용할 수 있게 함[^1]으로써 중첩타입(Nested Types)을 정의 할 수있습니다. [^1]: 예를 들면, 구조체 안에 클래스를 정의하고 그 클래스 안에 다시 열거형 또는 사용자가 정의한 구조체를 넣을 수있다는 얘기.
기존 타입안에 새로운 타입을 중첩하기 위해서는, 기존 타입이 둘러싸고 있는 중괄호('{','}') 안에서 정의를 작성합니다. 이러한 유형은 필요로 하는 만큼 여러 수준으로 중첩 될 수 있습니다.
아래의 예제에서는 블랙잭 게임에서 사용되는 게임 카드를 모델로 하는 BlackjackCard
구조체를 정의하고 있습니다. BlakcJack
구조체는 내부에 Suit
와 Rank
라는 이름의 두개의 열거형 타입을 가지고 있습니다.
블랙잭 게임에서 에이스 카드는 1또는 11의 값을 가지고 있습니다. 이러한 요소는 Values
라는 구조체에 의해 표현됩니다. Values
구조체는 Rank
열거형 내부에 중첩되어 있습니다.
struct BlackjackCard {
// nested Suit enumeration
enum Suit: Character {
case Spades = "♠", Hearts = "♡", Diamonds = "♢", Clubs = "♣"
}
// nested Rank enumeration
enum Rank: Int {
case Two = 2, Three, Four, Five, Six, Seven, Eight, Nine, Ten
case Jack, Queen, King, Ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .Ace:
return Values(first: 1, second: 11)
case .Jack, .Queen, .King:
return Values(first: 10, second: nil)
default:
return Values(first: self.toRaw(), second: nil)
}
}
}
// BlackjackCard properties and methods
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.toRaw()),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
Suit
열거형은 4가지 슈트[^2]들과 슈트의 그에 해당하는 Character
심볼 값을 함께 나타냅니다.
[^2]: 블랙잭에서 슈트란 카드에 있는 무늬를 말합니다.
Rank
열거형은 13가지 카드의 랭크와 그에 해당하는 Int
값을 나타냅니다. (Int
형의 숫자 값은 Jack, Queen, King, Ace 카드에는 사용되지 않습니다.)
위의 코드를 보면 알 수 있듯이, Rank
열거형은 Values
라는 추가적인 구조체를 포함하는 중첩구조의 형태를 취하고 있습니다. 이 구조는 대부분의 카드는 하나의 값을 가지지만, 에이스 카드는 두가지 값을 갖는다는 사실을 캡슐화합니다.Values
구조체는 다음과 표현하는 두가지 속성을 정의하고 있습니다.
Int
형의 firstInt?
형 또는 optional Int 형의 secondRank
도 Values
구조체의 인스턴스를 반환하는 계산된 values속성을 정의합니다. 이 계산된 속성은 카드의 순위를 고려하여 그 순위에 따라 적절한 값을 가지는 새로운 Values
인스턴스를 초기화 합니다. 이러한 속성은 Jack
, Queen
, King
, Ace
과 같은 특별한 값을 위해 사용합니다. 숫자카드의 경우에는 지정되어 있는 Int
값을 사용합니다.
BlackjackCard
구조체는 rank와 suit라는 두개의 속성을 가지고 있고, description이라는 계산된 속성도 정의하고 있습니다. 이 description 속성은 카드의 이름과 값에 대한 설명을 빌드하기 위해 rank와 suit에 저장된 값을 사용합니다.
BalckjackCard
구조체는 커스텀 이니셜라이저를 가지고 있지 않으므로, 앞 챕터의 구조체 타입을 위한 멤버 단위의 이니셜라이저(Memberwise Initializers for Structure Types)에서 설명한대로 암시적인 멤버단위 이니셜라이저(memberwise intializer)를 가지고 있습니다.
let theAceOfSpades = BlackjackCard(rank: .Ace, suit: .Spades)
println("theAceOfSpades: \(theAceOfSpades.description)")
// prints "theAceOfSpades: suit is ♠, value is 1 or 11
Rank
와 Suit
가 BlackjackCard
안에 중첩되어 있다고 해도 그들의 타입은 문맥으로 부터 추론될 수 았가 때문에 이 인스턴스의 초기화는 자신의 맴버 이름(.Ace와 .Spades)으로 열거형 멤버를 참조할 수 있습니다. 위의 예에서는 description속성이 올바르게 Space Ace 카드가 1 또는 11의 값을 가지고 있는지 확인합니다.
자신이 정의된 문맥 외부에서 중첩타입을 사용하려면, 자기를 포함하고 있는(중첩하고 있는)타입의 이름을 그 이름앞에 붙입니다.
let heartsSymbol = BlackjackCard.Suit.Hearts.toRaw()
// heartsSymbol is "♡"
위의 예를 보면, Suit
, Rank
, Values
와 같은 이름들은 자연스럽게 그들이 정의된 문맥에 의해 규정되기 때문에 의도적으로 짧게 유지할 수 있습니다.
Translator : Dongwoo Son (easthelper@gmail.com)
확장(Extensions)은 이미 존재하는 클래스, 구조체, 열거형 타입에 새 기능성을 추가합니다. 이는 원본 소스코드에 접근할 수 없는 타입들도 확장할 수 있습니다. (Retroactive modeling) 확장은 Objective-c 의 카테고리 와 유사합니다.
Swift 의 확장이 할수있는 것:
주의 만약 기존 타입에 새로운 기능성을 추가하기 위해 확장을 정의 한다면, 확장이 정의 되기 이전에 생성된 해당 타입의 모든 인스턴스들도 새 기능성이 적용이 됩니다.
extension
키워드로 확장을 선언합니다:
extension SomeType {
// SomeType에 추가할 새 기능
}
확장은 기존의 타입을 하나 이상의 프로토콜을 적용하기 위해서 확장시킬 수 있습니다. 이 경우 클래스 또는 구조체와 같은 방식으로 적용시킬 프로토콜 이름을 적습니다:
extension SomeType: SomeProtocol, AnotherProtocol {
// 프로토콜의 요구사항을 이곳에 구현
}
확장으로 프로토콜 준수의 추가는 Adding Protocol Conformance with an Extension 에 설명 되어 있습니다.
확장은 연산 인스턴스 속성과 연산 타입 속성을 기존의 타입에 추가할 수 있습니다. 이 예제는 거리 단위를 제공하기 위해 다섯개의 연산 인스턴스 속성을 Swift의 내장 Double 타입에 추가합니다.
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
println("One inch is \(oneInch) meters")
// prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
println("Three feet is \(threeFeet) meters")
// prints "Three feet is 0.914399970739201 meters"
이러한 연산 속성들은 Double
값이 특정 길이의 단위로 간주됨을 나타냅니다. 연산 속성들로 구현되었지만 부동소수점 리터럴 값에 점 문법으로 속성의 이름을 덧붙여 리터럴 값을 거리값으로 변환시킬 수 있습니다.
예를들어, 1.0
이라는 Double
값은 "1 미터"로 간주됩니다. 때문에 m
연산 속성은 self
를 반환합니다. - 1.m
표현은 1.0
Double
값 입니다.
다른 단위들은 미터 측정값으로 표현되기 위한 변환이 필요합니다. 1 킬로미터는 1000 미터와 같습니다. 따라서 km
연산 속성은 미터로 표현되기 위해 1_000.00
을 곱합니다. 같은 방식으로 1 미터는 3.28024 피트입니다. 따라서 피트를 미터로 바꾸기 위해 ft
연산 속성은 double
값을 3.28024
로 나눕니다.
이 속성들은 읽기 전용 속성이고 간결함을 위해 get
키워드 없이 사용될 수 있습니다. 속성들의 반환 값은 Double
형이기 때문에 Double
을 사용하는 어느 곳에서나 수학적 계산과 함께 사용 될 수 있습니다.
let aMarathon = 42.km + 195.m
println("A marathon is \(aMarathon) meters long")
// prints "A marathon is 42195.0 meters long"
주의 확장은 새로운 연산속성을 추가할 수 있습니다. 하지만 저장 속성 또는 기존 속성에 프로퍼티 옵저버를 추가할 수는 없습니다.
확장은 기존 타입에 새로운 이니셜라이저를 추가할 수 있습니다. 이는 다른 타입들이 여러분의 커스텀 타입을 이니셜라이저의 인자로 받을 수 있도록 하거나 또는 타입의 기본 구현에 포함되어 있지 않은 추가 적인 이니셜라이저 옵션을 제공할 수 있도록 확장하는 것을 가능하게 합니다.
확장은 새 convenience 이니셜라이저를 클래스에 추가할 수 있습니다. 하지만 새 designated 이니셜라이저 또는 디이니셜라이저를 추가할 수는 없습니다. designated 이니셜라이저와 디이니셜라이저는 반드시 본래의 클래스 구현에서 제공되어야 합니다.
주의 만약 확장을 사용해서 모든 저장 속성의 기본 값을 제공하는 값 타입에 새로운 이니셜라이저를 추가하고, 어떠한 커스텀 이니셜라이저도 정의하지 않았다면, 기본 이니셜라이저와 memberwise 이니셜라이저를 호출 할 수 있습니다. Initializer Delegation for Value Type에서 설명한 것 처럼 이니셜라이저를 값 타입의 본래 구현에 작성을 한 경우에는 해당 되지 않습니다.
아래의 예제는 직사각형을 나타내기 위한 커스텀 Rect
구조체를 정의합니다. 또한 모든 속성의 기본값이 0.0인 Size
와 Point
구조체를 정의합니다.
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}
Default Initailizers 에서 언급했던 것처럼 Rect
구조체는 모든 속성의 기본값을 제공하기 때문에 기본 이니셜라이저와 memberwise 이니셜라이저를 자동으로 받습니다. 이 이니셜라이저들은 새로운 Rect
인스턴스를 생성하기 위해 사용될 수 있습니다.
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
Rect
구조체에 특정 중심점과 크기를 받기 위한 추가 이니셜라이저를 제공하기 위해 확장할 수 있습니다.
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
이 새 이니셜라이저는 처음에 제공된 center
값과 size
값을 기반으로 적절한 origin point를 계산합니다. 그 다음 구조체의 자동 memberwise 이니셜라이저 init(origin:size:)
를 호출하여 새 origin 과 size 값을 적절한 속성에 저장합니다.
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
주의 확장으로 새 이니셜라이저를 제공 할 경우 이니셜라이저가 완료되었을 때 각 인스턴스가 완전히 초기화 되었는지 확인하는 책임은 작성자에게 있습니다.
확장은 기존 타입에 새 인스턴스 메소드와 타입 메소드를 추가할 수 있습니다. 다음 예제는 새 인스턴스 메소드 repetitions
를 Int
타입에 추가합니다:
extension Int {
func repetitions(task: () -> ()) {
for i in 0..self {
task()
}
}
}
repetitions
메소드는 매개변수가 없고 반환 값이 없음을 나타내는 하나의 ()->()
인자를 받습니다.
이 확장을 정의한 후에 여러번의 반복작업을 위해 어느 정수값에서 repetitions 메소드를 호출 할 수 있습니다.
3.repetitions({
println("Hello!")
})
// Hello!
// Hello!
// Hello!
호출을 더 간결하게 하기위해 후행 클로저 문법을 사용:
3.repetitions {
println("Goodbye!")
}
// Goodbye!
// Goodbye!
// Goodbye!
확장을 이용해 인스턴스 메소드 추가함으로써 인스턴스 스스로 또한 수정할 수 있습니다. self
또는 자신의 속성을 수정하는 구조체와 enumeration 메소드들은 반드시 인스턴스 메소드를 mutating
으로 표시 해야합니다.
아래 예제는 원래의 값을 제곱하는 새 mutating 메소드 square
를 Swift의 Int
타입에 추가합니다.
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt is now 9
확장은 기존 타입에 새 subscripts 를 추가할 수 있습니다. 이 예제는 integer subscript 를 Swift 내장 Int
타입에 추가합니다. 이 subscript [n]
는 수의 오른쪽으로 부터 n
번째 자리에 있는 10진수 숫자 하나를 반환합니다:
123456789[0]
returns 9
123456789[1]
returns 8
... 기타 등등:
extension Int {
subscript(digitIndex: Int) -> Int {
var decimalBase = 1
for _ in 1...digitIndex {
decimalBase *= 10
}
return (self / decimalBase) % 10
}
}
746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7
만약 Int
값이 길이가 요구된 인덱스 보다 적다면 수 왼쪽이 0들로 채워져 있다 여기고 0
을 반환합니다.
746381295[9]
// 다음을 요청한것 같이 처리되어 0 을 반환 합니다:
0746381295[9]
확장은 새 Nested 타입을 기존 클래스, 구조체, enumeration에 추가할 수 있습니다.
extension Character {
enum Kind {
case Vowel, Consonant, Other
}
var kind: Kind {
switch String(self).lowercaseString {
case "a", "e", "i", "o", "u":
return .Vowel
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
"n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z":
return .Consonant
default:
return .Other
}
}
}
이 예제는 새 nested enumeration을 Character
에 추가합니다. 이 Kind
enumeration 은 각 문자의 종류를 나타냅니다. 특히 문자가 표준 로마자에서 모음 또는 자음인지(강세나 지역적 다양성을 고려하지 않고), 또는 그 외의 문자인지를 나타냅니다.
이 예제는 또한 새 연산 인스턴스 속성 kind
을 Character
에 추가합니다. 이 속성은 해당 문자에 적절한 Kind
enumeration 멤버를 반환합니다.
이제 Character
값에서 nested enumeration 을 사용할 수 있습니다.
func printLetterKinds(word: String) {
println("'\(word)' is made up of the following kinds of letters:")
for character in word {
switch character.kind {
case .Vowel:
print("vowel ")
case .Consonant:
print("consonant ")
case .Other:
print("other ")
}
}
print("\n")
}
printLetterKinds("Hello")
// 'Hello' is made up of the following kinds of letters:
// consonant vowel consonant consonant vowel
printLetterinds
함수는 String
값을 받아서 문자열의 각 문자를 iterate 합니다. 각 문자에 대해서 kind
연산 속성에 따라 그 글자에 알맞는 종류를 출력합니다. 위 "Hello" 단어의 결과 처럼 printLetterinds
함수를 호출해서 단어 안의 모든 문자의 종류들을 출력할 수 있습니다.
주의
character.kind
는 이미 Character.Kind 타입으로 알려져 있기 때문에 모든Character.Kind
멤버 값들은switch
문에서Character.Kind.Vowel
보다.Vowel
같이 생략된 형식으로 쓸 수 있습니다.
프로토콜은 특정한 일이나 기능의 일부에 대한 메소드나 속성이나 다른 요구사항들의 전체적인 모습을 정의한다. 실제로 이런 요구사항들의 구현을 제공하지는 않고, 그 구현이 어떻게 보일지에 대해 명시한다. 이 요구사항들을 실제로 구현된 클래스, 구조체, 열거형 등에 그 프로토콜이 적용될 수 있다. 프로토콜의 요구사항을 만족하면 어떤 타입이라도 그 프로토콜에 일치한다(conform)라고 말한다.
프로토콜은 특정한 인스턴스 속성들, 인스턴스 메소드들, 타입 메소드들, 연산자들, 인덱스참조(subscript) 등을 갖는 타입을 가져야한다.
프로토콜을 클래스, 구조체, 열거체와 매우 비슷한 방법으로 정의한다.
protocol SomeProtocol {
// 프로토콜 정의가 여기 온다
}
타입을 정의하는 곳에서 타입의 이름 뒤에 콜론(:)으로 구분해서 프로토콜의 이름을 써서 프로토콜을 커스텀 타입에 적용시킨다. 여러 프로토콜을 쉼표(,)로 구분해서 사용할 수 있다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 구조체 정의가 여기 온다
}
클래스가 부모를 가질 때는 프로토콜들 앞에 부모 클래스를 명시하고 쉼표로 구분해서 적용한다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// 클래스 정의가 여기 온다
}
프로토콜은 특정한 이름과 속성을 갖는 인스턴스 속성과 타입 속성을 제공하는 타입이 될 수 있다. 프로토콜에는 이 속성이 저장된 속성이어야하는지 계산된 속성이어야 하는지에 대해 명시하지 않는다. 단지 속성의 이름과 타입만 명시할 뿐이다. 또한 각 속성에 대해 읽기(gettable)인지 읽기/쓰기(gettable/settable)가 필요한지 명시할 수 한다.
프로토콜의 속성에 읽기나 읽기/쓰기에 대한 명시가 있다면 그 속성은 저장된 상수값이나 읽기전용(read-only)의 계산된 값을 넣을 수 없다. 만약 읽기가 필요하다고만 명시가 되어있고 어떤 종류의 속성도 가능하며 필요하면 읽기를 만들어도 괜찮다.
속성 요구사항은 항상 var
키워드가 앞에 있는 변수 속성으로 선언된다. 읽기/쓰기 속성은 타입 뒤에 { get set }
을 써서 명시하며, 읽기는 { get }
으로 명시한다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
타입 속성은 class
키워드를 붙여서 정의할 수 있다. 구조체나 열거형에서 구현할 때는 static
을 붙이면 된다.
protocol AnotherProtocol {
class var someTypeProperty: Int { get set }
}
인스턴스 속성 하나만 필요로 하는 프로토콜 예제가 있다.
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed
프로토콜은 이름이 맞으면 종류에 관계없는 속성을 정의한다. 어떤 종류
여야하는지 명시하지는 않았고 그저 풀네임을 젱고할 수만 있으면 된다. String
타입의 읽기 가능한 fullName
이라는 인스턴스 속성을 가진 FullNamed
라는 요구사항만 명시되어있다.
FullyNamed
프로토콜이 적용되어있고 일치하는 간단한 구조체 예제다.
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed’
이 예제에서는 Person
이라고 불리는 구조체를 정의했고, 특정한 이름을 갖는다고 나타나있다. FullyNamed
프로토콜을 정의 첫번째 줄에 적용한 것이 보인다.
Person
의 인스턴스들은 String
타입의 fullName
속성 하나를 갖는다. FullNamed
프로토콜의 요구사항 하나와 일치하며, Person
이 확실하게 프로토콜에 일치한다는 것을 의미한다. (스위프트에서는 프로토콜의 요구사항이 채워지지 않으면 컴파일타임에 에러를 낸다.)
조금 더 복잡한 클래스가 있고, FullNamed
프로토콜을 적용했고 일치한다.
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise’
이 클래스에서는 계산된 읽기전용 속성으로 fullName
속성을 구현했다.
각 Starship
클래스 인스턴스는 name
을 필수로 prefix
를 옵션으로 갖는다. prefix
값이 존재하면 name
의 앞에 붙여서 우주선의 풀네임을 만들어 fullName
속성이 된다.
프로토콜은 일치하는 타입을 구현하기 위해 인스턴스 메소드들과 타입 메소드들을 요구사항으로 명시할 수 있다. 중괄호나 메소드 구현체(body)만 없을 뿐, 일반적인 인스턴스 메소드나 타입 메소드를 정의하는 것과 정확히 같은 방법으로 정의된다. 일반적인 메소드와 같은 규칙으로 가변길이의 변수도 가능하다.
노트
프로토콜은 일반적인 메소드들과 같은 문법을 사용하지만 인자로 기본값을 명시할 수 없다.
프로토콜에서 타입 속성을 정의할 때처럼 class
키워드를 타입 메소드 앞에 붙여주면 된다. 구조체나 열거형에서 구현할 때는 static
을 붙여주면 된다.
protocol SomeProtocol {
class func someTypeMethod()
}
인스턴스 메소드 하나만 있는 프로토콜의 예제다.
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator
프로토콜은 Double
을 리턴하는 random
인스턴스 메소드를 갖는 어떤 타입에도 일치할 수 있다. (프로토콜에서는 명시되지 않았지만 0.0
에서 1.0
사이의 값을 리턴할 것이라고 추정된다.)
RandomNumberGenerator
프로토콜만으로는 어떻게 난수를 생성할지에 대한 정보가 없다. 단지 새로운 난수를 만들어서 제공하는 발생기를 필요로 할 뿐이다.
RandomNumberGenerator
프로토콜을 적용하고 일치하는 클래스 구현체가 있다. 선형 합동 생성기(linear congruential generator)라는 의사난수 생성 알고리즘을 구현했다.
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c) % m)
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
println("Here's a random number: \(generator.random())")
// prints "Here's a random number: 0.37464991998171"
println("And another one: \(generator.random())")
// prints "And another one: 0.729023776863283"
가끔 메소드에서 자신의 인스턴스를 수정(혹은 변이)할 필요가 있다. 밸류 타입(즉, 구조체와 열거형)의 인스턴스 메소드에서 메소드의 func
앞에 mutating
키워드를 써서 소속된 인스턴스를 바꾸거나 인스턴스의 속성을 수정할 수 있게 명시한다. 이 과정은 인스턴스 메소드 내에서 밸류 타입의 수정에서 설졍되어있다.
프로토콜이 적용된 타입의 인스턴스를 변이할 수 있다고 인스턴스 메소드에 명시하려면 프로토콜 정의세ㅓ mutating
키워드를 추가하면 된다. 이 프로토콜이 적용된 구조체와 열거형은 요구사항을 만족한다.
노트
프로토콜을
mutating
이라고 명시하면 클래스에서 메소드를 구현할 때는mutating
키워드를 쓰지 않아도 된다.mutating
키워드는 구조체와 열거형에서만 쓰인다.
아래에는 Togglable
이라는 프로토콜 예제인데, toggle
이라는 인스턴스 메소드 하나만 정의되어있다. 이름에서 알 수 있듯 toggle
메소드는 보통 타입의 속성을 변환하는 것인데 프로토콜에 일치하는 타입의 속성을 토글하거나 반전한다.
toggle
메소드는 Togglable
프로토콜 정의에서 mutating
키워드로 이 메소드를 호출했을 때 인스턴스의 상태가 변이될 것을 예상할 수 있다.
protocol Togglable {
mutating func toggle()
}
Togglable
프로토콜을 구조체나 열거형으로 구현하려면, mutating
이 명시된 toggle
메소드를 구현해야 프로토콜에 일치할 수 있다.
아래 예제는 OnOffSwitch
라는 열거형이다. 이 열거형은 On
과 Off
두가지 상태 사이를 토글한다. 열거형의 toggle
구현체는 Togglable
프로토콜의 요구사항에 맞게 mutating
이 명시되어 있다.
enum OnOffSwitch: Togglable {
case Off, On
mutating func toggle() {
switch self {
case Off:
self = On
case On:
self = Off
}
}
}
var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()
// lightSwitch은 이제 .On과 같다.
프로토콜은 그 자체로 어떤 기능도 갖고 있지 않다. 하지만 어떤 프로토콜도 코드에서 다른 타입처럼 쓰일 수 있다.
왜냐하면 프로토콜도 타입이므로 다른 타입들이 쓰이는 곳에서 사용될 수 있다.
노트
프로토콜이 타입이므로 스위프트의 다른 타입(
Int
,String
,Double
같은)처럼 이름을 대문자(FullyNamed
나RandomNumberGenerator
처럼)로 사용할 수 있다.
타입으로 프로토콜을 사용하는 예제다.
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}
이 예제에서는 n면체의 Dice
라는 새로운 클래스가 정의되어있다. Dice
의 인스턴스는 면을 얼마나 가지고 있는지를 나타내는 sides
라는 정수 속성과 주사위를 굴렸을 때 난수를 생성해주는 generator
속성을 가지고 있다.
generator
속성은 RandomNumberGenerator
타입의 속성이다. 그러므로 RandomNumberGenerator
프로토콜을 적용한 어떤 타입의 인스턴스라도 할당할 수 있다.
Dice
는 초기 상태를 설정하는 생성자도 가지고 있다. 생성자는 RandomNumberGenerator
타입의 generator
를 인자로 받는다.
새로운 Dice
인스턴스를 만들 때 프로토콜에 일치하는 어떤 타입도 인자로 넘길 수 있다.
Dice
는 하나의 인스턴스 메소드 roll
이 있는데, 1에서 면의 수 사이에 해당하는 정수를 리턴한다. 이 메소드에서는 생성기의 random
메소드를 호출해서 0.0
과 1.0
사이의 난수를 받아 정확한 범위의 값을 만든다. generator
가 RandomNumberGenerator
를 적용하고 있기 때문에 확실하게 rondom
메소드를 가지고 있다.
여기 LinearCongruentialGenerator
인스턴스를 난수생성기로 받는 6면체의 Dice
클래스가 어떻게 사용되는지 예제가 있다.
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
println("랜덤한 주사위값은 \(d6.roll())")
}
// 랜덤한 주사위값은 3
// 랜덤한 주사위값은 5
// 랜덤한 주사위값은 4
// 랜덤한 주사위값은 5
// 랜덤한 주사위값은 4
위임은 클래스나 구조체가 다른 타입의 인스턴스에게 책임의 일부를 넘길(혹은 위임할) 수 있는 디자인 패턴이다. 이 디자인 패턴에서는 위임된 책임을 캡슐화하는 프로토콜을 정의하는데, 거기에 일치하는 타입(대리자delegate로 알려진)은 위임받은 기능이 있다고 보장된다. 위임은 특정 액션에 대해 응답하거나, 외부에서 온 정보가 어떤 타입인지에 관계없이 데이터를 처리할 때 사용할 수 있다.
아래에 주사위를 사용한 보드게임에 두가지 프로토콜이 정의되어있다.
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate {
func gameDidStart(game: DiceGame)
func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(game: DiceGame)
}
DiceGame
프로토콜은 주사위를 포함하는 어떤 게임에도 적용할 수 있는 프로토콜이다. DiceGameDelegate
프로토콜은 DiceGame
의 진행을 기록할 수 있는 어떤 타입에도 적용할 수 있는 프로토콜이다.
앞서 흐름 제어에서 소개되었던 뱀과 사다리의 수정 버전이다. 이 버전에서는 주사위 굴리기를 위해 Dice
인스턴스를 사용한 DiceGame
프로토콜을 적용했고 과정을 DiceGameDelegate
에 알리기 위해 DiceGame
프로토콜을 적용했다.
class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
var square = 0
var board: Int[]
init() {
board = Int[](count: finalSquare + 1, repeatedValue: 0)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
}
var delegate: DiceGameDelegate?
func play() {
square = 0
delegate?.gameDidStart(self)
gameLoop: while square != finalSquare {
let diceRoll = dice.roll()
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
switch square + diceRoll {
case finalSquare:
break gameLoop
case let newSquare where newSquare > finalSquare:
continue gameLoop
default:
square += diceRoll
square += board[square]
}
}
delegate?.gameDidEnd(self)
}
}
뱀과 사다리 게임에 대한 설명을 원하면 흐름 제어 챕터의 break 섹션에 나와있다.
이 버전에서는 DiceGame
을 적용한 SnakesAndLadders
라는 클래스로 이루어졌다. dice
속성과 play
메소드을 가지고 있어 프로토콜에 일치한다. (dice
속성은 상수 속성으로 선언되었는데, 일단 생성되고 난 뒤에 변경될 필요가 없으며 프로토콜의 요구는 읽기만 가능하면 된다.)
뱀과 사다리 게임보드 설정은 클래스의 init()
생성자 내에서 이루어진다. 게임 로직 전체는 프로토콜의 play
메소드 내로 옮겨졌고, 주사위를 굴린 값을 얻기 위해 dice
속성을 필요로 하는 프로토콜을 사용한다.
delegate
속성은 DiceGameDelegate
옵션으로 되어있는데, 게임을 실행하는데 대리자가 꼭 필요하지는 않아서이다. 옵션값이기 때문에 delegate
속성은 자동으로 nil
을 초기값으로 받는다. 그리고 나서 게임을 초기화할 때 적절한 위임자를 속성으로 받을 수도 있다.
DiceGameDelegate
는 게임의 진행을 기록하기 위해 3가지 메소드를 제공한다. play
메소드 내부에서 사용되며 새로운 게임을 시작할 때, 턴이 시작될 때, 게임이 끝날 때 호출된다.
delegate
속성은 DiceGameDelegate
타입의 옵션값이기 때문에 play
메소드는 대리자에서 메소드를 호출할 때마다 옵션 연쇄를 사용한다. delegate
속성이 nil이면, 이 대리자는 에러없이 호출을 실패한다. deleagte
속성이 nil이 아니면 메소드를 호출하고 SnakesAndLadders
인스턴스를 인자로 넘긴다.
다음 예제는 DiceGameTracker
라는 클래스로, DiceGameDelegate
프로토콜이 적용되었다.
class DiceGameTracker: DiceGameDelegate {
var numberOfTurns = 0
func gameDidStart(game: DiceGame) {
numberOfTurns = 0
if game is SnakesAndLadders {
println("뱀과 사다리의 새 게임을 시작한다")
}
println("게임은 \(game.dice.sides)면체 주사위를 사용할 것이다")
}
func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
++numberOfTurns
println("주사위는 \(diceRoll)")
}
func gameDidEnd(game: DiceGame) {
println("게임은 \(numberOfTurns)턴을 사용했다")
}
}
DiceGameTracker
에서는 DiceGameDelegate
에서 필요한 세가지 메소드를 모두 구현되어있다. 세가지 메소드를 사용해서 게임에서 몇턴이 진행되었는지 기록한다. 게임이 시작하면 numberOfTurns
속성을 0을 초괴화하고, 새 턴이 시작할 때마다 증가시키고, 게임이 끝났을 때 총 몇턴이 지났는지 출력한다.
위에서 나온 gameDidStart
의 구현에서는 game
인자를 사용해 게임을 플레이하려고 할 때 안내 문구를 출력한다. game
인자는 SnakesAndLadders
타입이 아니라 DiceGame
타입을 갖는다. gameDidStart
는 DiceGame
프로토콜에 있는 메소드와 속성들만 사용하고 접근한다. 하지만 메소드에서는 타입 케스팅을 통해 인스턴스가 어떤 타입인지 확인할 수도 있다. 이 예제에서는 game
이 실제로 SnakesAndLadders
의 인스턴스인지 확인하고 맞다면 적절한 문구를 출력한다.
gameDidStart
는 인자로 받은 game
의 dice
속성에도 접근한다. game
은 DiceGame
프로토콜에 일치한다고 되어있으니 dice
속성을 가지고 있을 것이고 gameDidStart
메소드는 어떤 종류의 게임인지에 관계없이 주사위의 sides
속성을 출력할 수 있다.
DiceGameTracker
가 어떻게 작동하는지 아래 있다.
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// 뱀과 사다리 새 게임을 시작한다
// 게임은 6면체 주사위를 사용할 것이다
// 주사위는 3
// 주사위는 5
// 주사위는 4
// 주사위는 5
// 게임은 4턴을 사용했다
이미 존재하는 타입의 소스에 접근할 수 없어도 그 타입에 프로토콜을 적용하고 일치하도록 확장할 수 있다. 확장은 새 속성들, 메소드들, 인덱스 참조 등을 이미 존재하는 타입에 추가할 수 있고, 프로토콜에서 필요로 하는 요구사항들을 추가할 수도 있다. 확장에 대해 더 많은 정보는 확장 챕터에 있다.
노트
확장을 타입에 추가하는 순간 이미 만들어놓은 인스턴스들에서도 프로토콜이 적용되고 일치하게 된다.
예를 들어, TextRepresentable
이라는 프로토콜은 타입을 텍스트로 표현하는 방법을 구현할 수 있다. 인스턴스의 설명이 될 수도 있고, 현재 상태의 텍스트 표현이 될 수도 있다.
protocol TextRepresentable {
func asText() -> String
}
위에서 본 Dice
클래스에 TextRepresentable
을 적용하고 일치시킬 수 있다.
extension Dice: TextRepresentable {
func asText() -> String {
return "\(sides)면체 주사위"
}
}
확장은 Dice
를 위에서 구현했던 것과 정확히 같은 방법으로 새로운 프로토콜을 적용한다. 프로토콜 이름 뒤에 콜론으로 구분해서 프로토콜의 이름을 적고 중괄호 안에 프로토콜의 요구사항들 전부를 구현하면 된다.
이제 어떤 Dice
인스턴스들도 TextRepresentable
로 처리할 수 있다.
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
println(d12.asText())
// "12면체 주사위" 출력
비슷하게 SnakesAndLadders
게임 클래스도 TextRepresentable
프로토콜을 적용하고 일치하도록 확장할 수 있다.
extension SnakesAndLadders: TextRepresentable {
funcs asText() -> String {
return "뱀과 사다리 게임은 \(finalSquare)"칸
}
}
println(game.asText())
// "뱀과 사다리 게임은 25칸" 출력
타입이 이미 프로토콜의 모든 요구사항에 일치하고 있지만 프로토콜을 적용한다고 명시하지 않았을 때, 빈 확장과 함께 프로토콜을 적용시킬 수 있다.
struct Hamster {
var name: String
func asText() -> String {
return "햄스터 이름은 \(name)"
}
}
extension Hamster: TextRepresentable {}
이제 Hamster
의 인스턴스들은 TextRepresentable
을 타입으로 사용할 수 있다.
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
println(somethingTextRepresentable.asText())
// "햄스터 이름은 Simon" 출력
노트
타입에서 요구사항을 만족했다고 자동으로 프토토콜이 적용되지는 않는다. 항상 명시적으로 프로토콜의 적용을 선언해줘야 한다.
프로토콜은 타입으로서의 프로토콜에서 이야기한 것처럼 배열이나 사전같은 콜렉션에 저장되는 타입으로 사용할 수 있다.
여기 TextRepresentable
의 배열을 만든 예제가 있다.
let things: TextRepresentable[] = [game, d12, simonTheHamster]
배열에서 아이템들을 반복하면서 각 아이템을 텍스트로 출력하는 것이 가능하다.
for thing in things {
println(thing.asText())
}
// 뱀과 사다리 게임은 25칸
// 12면체 주사위
// 햄스터 이름은 Simon
thing
상수는 Dice
나 DiceGame
이나 Hamster
타입이 아니고 TextRepresentable
타입이다. 하지만 TextRepresentable
은 asText
메소드를 가지고 있기 때문에 반복문에서 thing.asText
를 안전하게 호출할 수 있다.
프로토콜은 하나 이상의 프로토콜을 상속받을 수 있고, 그 요구사항들 위에 다른 요구사항을 추가할 수도 있다. 프로토콜 상속 문법은 클래스 상속의 문법과 비슷하지만 쉼표를 구분해서 여러 프로토콜을 나열할 수 있다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 프로토콜 정의가 여기 온다
}
위의 TextRepresentable
프로토콜을 상속받는 예제가 있다.
protocol PrettyTextRepresentable: TextRepresentable {
func asPrettyText() -> String
}
이 예제에서는 TextRepresentable
을 상속받는 PrettyTextRepresentable
라는 새로운 프로토콜을 정의한다. PrettyTextRepresentable
을 적용한 것들은 TextRepresentable
의 요구사항을 모두 만족해야하고, 추가로 PrettyTextRepresentable
의 요구사항도 만족해야한다. 이 예제에서는 String
을 리턴하는 asPrettyText
라는 인스턴스 메소드 하나가 요구사항으로 추가되었다.
PrettyTextRepresentable
를 적용하고 일치하게 SnakesAndLadders
클래스를 확장할 수 있다.
extension SnakesAndLadders: PrettyTextRepresentable {
func asPrettyText() -> String {
var output = asText() + ":\n"
for index in 1...finalSquare {
switch board[index] {
case let ladder where ladder > 0:
output += "▲ "
case let snake where snake < 0:
output += "▼ "
default:
output += "○ "
}
}
return output
}
}
이 확장은 PrettyTextRepresentable
프로토콜을 적용하고 SnakesAndLadders
타입에서 asPrettyText
메소드를 구현했다. PrettyTextRepresentable
가 적용되었다면 TextRepresentable
도 적용해야하므로, asPrettyText
의 구현이 TextRepresentable
의 asText
메소드를 호출하는 것으로 시작한다. asText
에 콜론과 줄넘김을 붙이는 것을 시작으로 출력값을 만든다. 보드의 배열을 돌면서 각 칸에 해당하는 특수문자(emoji)를 붙인다.
이제 메소드구현으로 SnakesAndLadders
인스턴스의 내역을 보기좋게 표시하는데 사용할 수 있다.
println(game.asPrettyText())
// 뱀과 사다리 게임은 25칸:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
한번에 여러 프로토콜에 일치하는 타입이 필요할 때 유용하게 쓸 수 있다. 프로토콜 합성으로 여러 프로토콜을 하나의 요구사항으로 합칠 수 있다. 프로토콜 합성은 protocol<SomeProtocol, AnotherProtocol>
같은 형태를 가진다. 꺾쇠(<>) 안에 쉼표로 구분해서 원하는만큼 프로토콜을 명시할 수 있다.
여기 Named
와 Aged
두가지 프로토콜을 합성해서 하나의 함수 파라미터로 사용하는 예제가 있다.
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
println("\(celebrator.name)의 \(celebrator.age)번째 생일을 축하합니다!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(birthdayPerson)
// "Malcolm의 21번째 생일을 축하합니다!" 출력
이 예제에서는 String
타입의 name
이라는 읽기 속성 하나를 요구사항으로 가지는 Named
라는 프로토콜을 정의한다. 또한 Int
타입의 age
라는 읽기 속성 하나를 요구사항으로 가지는 Aged
도 정의한다. 두 프로토콜 모두 Person
구조체에 적용된다.
wishHappyBirthday
라는 함수를 만들어서 celebrator
라는 인자를 하나 받는다. 이 인자의 타입은 protocol<Named, Aged>
이며 "Named
와 Aged
프로토콜에 모두 일치하는 어떤 타입"을 의미한다. 어떤 특정한 타입이 인자로 넘어오는지는 관계없으나 요구하는 프로토콜 양쪽 다 일치해야한다.
그리고나서 birthdayPerson
이라는 Person
의 인스턴스를 만들어서 wishHappyBirthday
라는 함수의 인자로 넘긴다. Person
이 프로토콜 양쪽 다 일치하기 때문에 유효하며 wishHappyBirthday
함수에서 생일축하 인사를 출력할 수 있다.
노트
프로토콜 합성은 새로 영구적으로 프토토콜 타입을 만드는 것이 아니다. 합성에 있는 모든 프로토콜의 요구사항을 합친 하나의 프로토콜을 임시로 만드는 것이다.
타입 캐스팅에서 설명했던 것처럼 특정 프로토콜로 캐스팅하기 위해서 프로토콜 일치를 확인하는데 is
와 as
연산자를 사용할 수 있다. 타입을 확인하고 캐스팅하는 것과 정확히 같은 방법으로 프로토콜을 확인하고 캐스팅할 수 있다.
is
연산자에서는 인스턴스가 프로토콜과 일치하면 true
, 아니면 false
를 리턴nil
이 된다as
연산자는 강제로 다운캐스팅하고 실패하면 런타임 에러가 난다.Double
타입의 area
읽기 속성 하나를 요구사항으로 갖는 HasArea
프로토콜 예제가 있다.
@objc protocol HasArea {
var area: Double { get }
}
노트
HasArea
프로토콜 앞에 보이듯 프로토콜 일치를 확인하기 위해서는@objc
속성(attribute)을 명시해줘야한다. 코코아와 Objective-C를 스위프트와 사용하기(Using Swift with Cocoa and Objective-C)에서 설명하듯 이 속성은 Objective-C 코드에서 인식할 수 있을 것이라는 것을 명시한다. Objective-C를 함께 쓰지 않더라도 프로토콜 일치를 확인하고 싶다면objc
를 프로토콜에 명시해줘야한다.
@objc
프로토콜은 구조체나 열거형은 불가능하고 클래스에만 적용할 수 있다.
HasArea
프로토콜과 일치하는 Circle
과 Country
두가지 클래스가 있다.
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}
Circle
클래스는 저장된 radius
속성을 사용해 area
속성을 계산된 속성으로 구현했다. Country
클래스에서는 직접 저장된 속성으로 area
속성을 구현했다. 두가지 클래스 모두 HasArea
프로토콜에 정확히 일치한다.
여기 Animal
이라는 클래스가 있고 HasArea
프로토콜에 일치하지 않는다.
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle
, Country
, Animal
클래스는 공통된 부모 클래스를 갖지는 않는다. 다만 이 클래스들과 클래스들의 인스턴스들 모두 AnyObject
배열의 값으로 초기화되는데 사용할 수 있다.
let objects: AnyObject[] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
Circle
인스턴스는 반지름 2로, Country
인스턴스는 영국의 면적으로, Animal
인스턴스는 다리 4개로 초시화되어 배열 표기를 통해 objects
배열에 초기화되었다.
objects
배열은 순환가능하며, 배열 내 각각의 객체는 HasArea
프로토콜에 일치하는지 확인할 수 있다.
for object in objects {
if let objectWithArea = object as? HasArea {
println("넓이는 \(objectWithArea.area)")
} else {
println("넓이를 가지고 있지 않다")
}
}
// 넓이는 12.5663708
// 넓이는 243610.0
// 넓이를 가지고 있지 않다
배열 내의 객체가 HasArea
프로토콜에 일치할 때마다 as?
연산자를 통해 objectWithArea
라는 상수값으로 옵션값을 받을 수 있다. objectWithArea
상수는 HasArea
타입이며 area
속성을 접근할 수도 있고 출력도 가능하다.
캐스팅 과정에서 객체들이 변하지는 않는다. 여전히 Circle
, Country
, Animal
객체다. 하지만 objectWithArea
상수에 저장되면 HasArea
타입으로만 사용할 수 있고 area
속성에만 접근할 수 있다.
프로토콜에서 선택적 요구사항을 정의할 수 있다. 프로토콜에 일치하기 위해서 이 요구사항들을 구현하지 않아도 된다. 선택적 요구사항들은 정의 앞에 @optional
키워드가 붙는다.
선택적 요구사항은 프로토콜에 일치하도록 요구사항이 구현되었는지 여부를 확인하는 옵션 연쇄와 같이 사용될 수 있다. 옵션 연쇄에 대해서는 옵션 연쇄 챕터에 정보가 나와있다.
요구사항(의 메소드나 이름)이 쓰일 때 someOptionalMethod?(someArgument)
처럼 이름 뒤에 물음표가 붙는 구현을 확인할 수 있다.
선택적 속성 요구사항과 값을 리턴하는 선택적 메소드 요구사항은 그걸 접근해서 호출할 때 적절한 타입의 옵션값을 항상 반환하며, 구현이 되어있지 않을 수 있는 선택적 요구사항의 상태를 보여준다.
노트 프로토콜의 선택적 요구사항들은
@objc
속성이 명시되어있을 때만 사용할 수 있다. Objective-C와 같이 사용하지 않더라도 선택적 요구사항을 사용하고 싶다면@objc
속성을 명시해야한다. 구조체나 열거형이 아닌 클래스에서만@objc
프로토콜을 적용할 수 있다. 따라서 선택적 요구사항을 사용하기 위해@objc
를 프로토콜에 명시했다면 프로토콜은 클래스 타입에만 적용될 수 있다.
다음 예제는 Counter
라는 정수 카운터를 구현한 것으로 증가량을 외부에 제공하기 위해 사용한다. 선택적 요구사항 2가지를 가진 CounterDataSource
프로토콜이 정의되어있다.
@objc protocol CounterDataSource {
@optional func incrementForCount(count: Int) -> Int
@optional var fixedIncrement: Int { get }
}
CounterDataSource
프로토콜에는 incrementForCount
라는 선택적 메소드 요구사항과 fixedIncrement
라는 선택적 속성 요구사항이 정의되어있다.
이 요구사항들은 Counter
인스턴스에 증가량을 전하기 위한 두가지 방법이다.
노트 엄밀히 이야기해서 프로토콜 요구사항을 전혀 구현하지 않고도
CounterDataSource
에 일치하는 클래스를 만들 수 있다. 둘 다 선택적 요구사항이기 때문이다. 기술적으로는 가능하지만 괜찮은 방법은 아니다.
아래 정의한 Counter
클래스는 CounterDataSource?
타입을 dataSource
라는 선택적 속성으로 가지고 있다.
@objc class Counter {
var count = 0
var dataSource: CounterDataSource?
func increment() {
if let amount = dataSource?.incrementForCount?(count) {
count += amount
} else if let amount = dataSource?.fixedIncrement? {
count += amount
}
}
}
Counter
클래스는 count
속성에 현재값을 저장한다.
그리고 increment
라는 메소드도 가지고 있는데 불릴 때마다 count
속성을 증가시킨다.
increment
메소드는 데이터 소스에서 incrementForCount
가 구현되어있는지 확인하고 증가량을 가져온다. 옵션 연쇄로 count
를 인자로 넘기는 incrementForCount
를 호출한다.
여기 두 단계의 옵션 연쇄가 있다. 첫번째로는 dataSource
가 nil
일 수도 있으니 아닐 때만 incrementForCount
를 호출하기 위해 물음표가 붙어있다. 두번째로는 dataSource
가 존재하긴 하지만 선택적 요구사항인 incrementForCount
가 구현되어있다는 보장이 없다. 그래서 incrementForCount
에도 물음표가 붙어 있다.
incrementForCount
를 호출하는데 두가지 이유로 실패할 수 있어서 선택적 Int
값이 리턴된다. 참이면 incrementForCount
가 구현되어 있어서 CounterDataSource
정의에 따라 비선택적(non-optional) Int
값을 받는다.
incrementForCount
가 호출되고 나면 실제로 받은 Int
값이 amount
에 상수로 들어간다. 만약 선택적 Int
가 값을 가지고 있으면, 즉 대리자와 메소드가 둘 다 존재하면 메소드는 값을 리턴할테니 amount
의 실제값이 count
속성에 더해져 저장되고, 증가는 완료된다.
dataSource
가 nil이거나 incrementForCount
를 구현하지 않아서 incrementForCount
에서 값을 가져올 수 없다면 increment
메소드는 fixedIncrement
속성을 대신 가져온다.
fixedIncrement
속성은 선택적 요구사항이므로 끝에 물음표를 붙여서 실패할 수 있는 것을 명시한다. 앞에서처럼 CounterDataSource
프로토콜의 정의에서 fixedIncrement
는 비선택적 Int
속성이지만, 결과적으로 선택적 Int
값이 리턴된다.
호출될 때마다 3
을 리턴하는 간단한 CounterDataSource
구현이 있다. 선택적 fixedIncrement
속성을 구현했다.
class ThreeSource: CounterDataSource {
let fixedIncrement = 3
}
ThreeSource
의 인스턴스를 새로운 Counter
인스턴스의 데이터 소스로 사용할 수 있다.
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
println(counter.count)
}
// 3
// 6
// 9
// 12
새로운 Counter
인스턴스를 만드는 위의 코드에서는 ThreeSource
인스턴스를 데이터 소스로 사용하고, increment
메소드를 4번 호출한다. 예상대로 카운터의 count
속성은 increment
가 호출되는 3번동안 증가한다.
여기 TowardsZeroSource
라는 조금 더 복잡한 데이터 소스가 있다. Counter
인스턴스에서는 현재 count
값에서 0을 향해 올리거나 내린다.
class TowardsZeroSource: CounterDataSource {
func incrementForCount(count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
TowardsZeroSource
클래스는 CounterDataSource
프로토콜의 선택적 incrementForCount
메소드를 구현했고 count
인자값을 사용해 0을 향하도록 한다. count
가 이미 0이면 더이상 카운트할 필요가 없다고 명시하도록 0
을 리턴한다.
Counter
인스턴스에 TowardsZeroSource
인스턴스를 상요해서 -4
에서 0까지 증가시킨다. 카운터가 0에 도달하면 더이상 카운트하지 않는다.
Counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
println(counter.count)
}
// -3
// -2
// -1
// 0
// 0
Translator: Hoon H. (Eonil, drawtree@gmail.com)
제네릭 코드는 정의된 요구사항에 따라 유연하고 재사용 가능한 함수들을 쓸 수 있도록 해줍니다. 반복을 피하고 의도를 명확하고 추상적으로 나타낼 수 있는 코드를 쓸 수 있습니다.
제네릭스는 Swift의 가장 강력한 기능 중 하나이며, Swift 기본 라이브러리의 많은 부분이 제네릭 코드로 만들어져 있습니다. 눈치채지 못했을 수도 있지만, 사실 제네릭스는 이 Language Guide에 이미 전반적으로 사용되고 있습니다. 예를 들어, Swift의 Array
와 Dictionary
타입들은 모두 제네릭 타입입니다. Int
값을 담는 배열이나 String
값을 담는 배열을 만들 수 있습니다. 사실 어떤 타입의 배열든지 만들 수 있습니다. 비슷하게, 특정 형식의 값을 담는 사전(dictionary)도 만들 수 있으며, 선택 가능한 타입에는 어떤 제한도 없습니다.
여기에 두 개의 Int
값을 교체하는 swapTwoInts
라는 일반적인 비-제네릭 함수가 있습니다.
func swapTwoInts(inout a: Int, inout b: Int) {
let temporaryA = a
a = b
b = temporaryA
}
이 함수는 In-Out Parameters에 설명된 대로 두 값을 서로 바꾸기 위해 in-out 패러미터를 사용합니다.
swapTwoInts
함수는 b
의 원본값을 a
로, a
의 원본 값을 b
로 바꾸어 넣습니다. 두 개의 Int
변수들에 있는 값들을 바꾸기 위해 이 함수를 호출할 수 있습니다.
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
println("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// prints "someInt is now 107, and anotherInt is now 3"
해당 swapTwoInts
함수는 유용하지만, 오직 Int
값만 사용할 수 있습니다. 만약 두 개의 String
값이나, 두 개의 Double
값을 바꾸려면 swapTwoStrings
이나 swapTwoDoubles
같은 함수를 더 작성해야 합니다.
func swapTwoStrings(inout a: String, inout b: String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(inout a: Double, inout b: Double) {
let temporaryA = a
a = b
b = temporaryA
}
이제 아마 swapTwoInts
와 swapTwoStrings
, swapTwoDoubles
의 본체가 똑같다는 것을 눈치챘을 것입니다. 유일한 차이는 그들이 받아들이는(Int
와 String
, Double
) 값의 타입입니다.
어떤 형식의 값이든 바꿀 수 있는 함수를 쓴다면 훨씬 더 유용하고, 더 유연하게 생각될 것입니다. 이런 종류의 문제가 바로 제네릭이 해결할 수 있는 문제입니다. (제네릭 버전의 이 함수들은 아래에 정의되어 있습니다)
노트 이 모든 세 함수에서
a
와b
의 타입은 서로 같도록 정의되어 있습니다. 만약a
와b
의 타입이 같지 않다면 두 값을 바꾸는 것은 불가능할 것입니다. Swift는 타입-안전 언어이기에 (예를 들어)String
타입의 변수와Double
타입의 변수를 서로 바꾸도록 지원하지 않습니다. 그런 시도는 컴파일-타임 에러를 생성합니다.
제네릭 함수들은 어떤 타입과도 같이 동작합니다. 여기에 swapTwoValues
라 불리는, 위에 언급된 swapTwoInts
함수의 제네릭 버전이 있습니다:
func swapTwoValues<T>(inout a: T, inout b: T) {
let temporaryA = a
a = b
b = temporaryA
}
swapTwoValues
함수의 본체는 swapTwoInts
함수와 같습니다. 하지만, swapTwoValues
함수의 첫줄은 swapTwoInts
와 약간 다릅니다. 여기에 첫 줄의 비교가 있습니다:
func swapTwoInts(inout a: Int, inout b: Int)
func swapTwoValues<T>(inout a: T, inout b: T)
제네릭 버전의 함수는 (Int
나 String
, Double
같은) 실제 타입 이름 대신 placeholder 타입 이름을 사용합니다. (여기에서는 T
) placeholder 타입 이름은 T
가 되어야 하는 타입이 어떤 것인지 말하지 않지만, a
와 b
가 같은 타입이어야 한다는 것을 말합니다. T
의 자리에 사용될 실제 타입은 swapTwoValues
함수가 호출될 때마다 결정될 것입니다.
다른 차이점은 제네릭 버전의 swapTwoValues
함수 정의에서는 함수 이름(swapTwoValues
) 뒤에 placeholder 타입 이름(T
)이 꺽쇠(<T>
) 안에 따라온다는 것입니다. 꺽쇠(angle bracket)는 Swift에게 swapTwoValues
함수 정의 안에 있는 T
는 placeholder 타입이라는 것을 알려줍니다. T
가 placeholder 타입이므로, Swift는 T
라는 실제 타입을 찾지 않습니다.
swapTwoValues
함수는 어떤 타입의 두 값이든 사용할 수 있다는 것을 제외하면 이제 swapTwoInts
함수와 똑같이 호출될 수 있습니다. 단, 두 값의 타입은 서로 같아야 합니다. 매번 swapTwoValues
이 호출될 때마다 함수에 전달된 값의 타입에 따라 T
에 사용될 타입이 추정됩니다.
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
노트 위에 정의된
swapTwoValues
함수는 Swift 표준 라이브라리에 있는 제네릭 버전swap
함수에서 비롯되었습니다. 이 함수는 자동으로 앱 코드에 사용 가능합니다.swapTwoValues
의 기능이 필요하다면 스스로 이 기능을 작성하기보다는 Swift에 미리 정의된swap
함수를 쓰기 바랍니다.
위의 swapTwoValues
예시에서 placeholder 타입 T
는 타입 패러미터
의 한 예시입니다. 타입 패러미터는 함수 이름 바로 뒤에 (<T>
같이) 꺽쇠 쌍 안에 있으며, 하나의 placeholder 타입을 지정하고 이름을 붙입니다.
일단 한 번 지정되면, 타입 패러미터는 (swapTwoValues
함수의 a
나 b
패러미터들같이) 함수의 패러미터나, 리턴 타입 또는 함수 안에서의 타입 표시(annotation)에 사용될 수 있습니다. 타입 패러미터로 표현된 placeholder 타입은 매번 함수가 호출될 때마다 실제 타입으로 바뀌집니다. (위의 swapTwoValues
예시에서는 처음 함수 호출에서는 T
가 Int
로, 두번째 호출에서는 String
으로 바뀌었습니다.
꺽쇠 안에 쉼표로 구분된 여러개의 타입 패러미터를 써서 하나 이상의 타입 패러미터들을 제공할 수 있습니다.
제네릭 함수나 제네릭 타입이 하나의 placeholder 타입(위의 swapTwoValues
제네릭 함수나 Array
같이 하나의 타입만 저장하는 제네릭 컬렉션)만 참조하는 간단한 경우, 타입 패러미터 이름에는 전통적으로 단일 문자 T
를 사용합니다만, 패러미터 이름으로는 어떤 유효한 식별자든지 사용될 수 있습니다.
여러개의 패러미터를 지닌 좀 더 복잡한 제네릭 함수들이나 제네릭 타입들을 정의한다면, 좀 더 설명적인 타입 이름을 제공하는 것이 더 유용할 것입니다. 예를 들어, Swift의 Dictionary
타입은 각각 키와 값을 위한 두 개의 패러미터들을 갖고 있습니다. 만약 Dictionary
을 스스로 작성해야 한다면 해당 타입 패러미터의 목적을 기억히기 쉽게 하기 위해 KeyType
와 ValueType
같은 이름을 사용하고 싶을 것입니다.
노트 값이 아닌 타입을 위한 placeholder임을 표시하기 위해, 타입 패러미터 이름은 항상
UpperCamelCase
로 지정하세요. (예:T
,KeyType
)
Swift는 제네릭 함수는 물론, 제네릭 타입도 제공합니다. 이들은 Array
나 Dictionary
와 비슷하게 어떤 타입과도 동작하는 사용자-지정 클래스, 구조체, 열거형들입니다.
이 섹션에서는 어떻게 Stack
이라는 제네릭 컬렉션 타입을 만드는지를 보여줍니다. 스택은 배열과 비슷한 하나의 정렬된 값 집합이지만, Swift의 Array
타입보다는 좀 더 제한된 연산만 사용 가능합니다. 배열은 배열의 어떤 위체에나 새로운 아이템이 추가되거나 삭제되도록 지원합니다. 하지만 스택은 컬렉션의 끝에 추가하는 것만 허용합니다. (새 값을 스택에 push한다는 표현으로 알려져 있습니다.) 비슷하게 스택은 컬렉션의 끝에 있는 아이템만 삭제하도록 허용합니다. (값을 스택에서 pop한다는 표현으로 알려져 있습니다)
노트 스택 컨셉은 네비게이션 계층 상에서 뷰-제어기를 모델링하기 위해
UINavigationController
클래스에 사용되었습니다. 하나의 뷰-제어기를 네비게이션 스택에 추가(또는 push)하기 위해UINavigationController
클래스의pushViewController:animated:
메서드를 호출하거나 , 하나의 뷰-제어기를 네비게이션 스택에서 삭제(또는 pop)하기 위해popViewControllerAnimated:
메서드를 호출할 수 있습니다. 스택은 컬렉션에서 엄격한 "후입선출(last in, first oout)" 방식을 관리할 때 유용합니다.
아래 그림은 스택의 push/pop 동작을 보여줍니다.
여기 Int
값 스택으로 어떻게 비-제네릭 버전의 스택을 쓰는지 보여줍니다.
struct IntStack {
var items = Int[]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
해당 구조체는 스택에 값을 저장하기 위해 items
라 불리는 Array
속성을 사용합니다. Stack
은 스택에 값을 push하고 pop하기 위해 push
와 pop
이라는 두 개의 메서드를 제공합니다. 이 메서드들은 구조체의 items
배열을 수정(mutate)해야 하므로 mutating
으로 표시되어 있습니다.
어쨌든, 위에 보여진 IntStack
타입은 Int
값들만 사용할 수 있습니다. 어떤 타입의 값이든 사용할 수 있는 제네릭 Stack
클래스를 정의할 수 있다면 아주 유용할 것입니다.
여기에 같은 코드의 제네릭 버전이 있습니다:
struct Stack<T> {
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}
제네릭 버전의 Stack
이 Int
타입 대신 placeholder 타입 패러미터 T
를 사용하는 것 외에는 본질적으로는 비-제네릭 버전과 같음에 주목하세요. 이 타입 패러미터는 구조체 이름 바로 뒤에 꺽쇠 쌍(<T>
)으로 둘러싸여 쓰여 있습니다.
T
는 나중에 제공될 "어떤 타입 T
"를 위한 placeholder 이름을 정의합니다. 이 미래의 타입은 구조체 정의 어디에서나 "T
"로 참조될 수 있습니다. 이 경우에는, T
는 세 곳에서 placeholder로 쓰였습니다.
T
타입 값의 빈 배열로 초기화된 items
이라 불리는 속성.T
타입이 되어야 하는 item
이라는 하나의 패러미터를 가지는 push
라는 메서드.T
타입 값을 반환하는 pop
메서드.초기화 문법으로 새 인스턴스를 만들 때, 개별 스택의 실제 타입을 타입 이름 뒤에 오는 꺽쇠 기호 안에 써서 Array
나 Dictionary
와 비슷하게 Stack
의 인스턴스를 만들 수 있습니다.
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// the stack now contains 4 strings
이 그림은 네 개의 값을 push한 뒤에 stackOfStrings
가 어떻게 되는지를 보여지는지를 보여줍니다.
한 값을 pop하면 스택의 최상위에서 해당 값을 지우고 반환합니다. "cuatro"
:
let fromTheTop = stackOfStrings.pop()
// fromTheTop is equal to "cuatro", and the stack now contains 3 strings
최상위 값을 pop한 뒤 스택은 이렇게 됩니다.
Stack
은 제네릭 타입이므로 Array
나 Dictionary
와 비슷하게 Swift의 어떤 타입과도 같이 사용될 수 있습니다.
swapTwoValues
함수와 Stack
타입은 어떤 타입과도 같이 작동합니다. 하지만, 어떨 때는 제네릭 함수와 제네릭 타입에 같이 사용될 수 있는 타입을 제약(type constraint)하는 것이 더 유용합니다. 타입 제약은 타입 패러미터가 특정 클래스에서 상속되어야 한다거나 특정 프로토콜 또는 프로토콜 합성을 만족해야 한다거나 하는 것이 될 수 있습니다.
예를 들어, Swift의 Dictionary
타입은 사전 키로 사용될 수 있는 타입을 제한합니다. Dictionaries에 설명된대로, 사전 키의 타입은 반드시 해시-가능(hashable)해야 합니다. 이는 키가 자신을 유일하게 식별할 수 있는 방식을 제공해야 한다는 뜻입니다. Dictionary
에서는 특정 키가 이미 등록되어 있는지 여부를 판별하기 위해 해시-가능한 키가 필요합니다. 이 요구사항이 없다면 Dictionary
는 특정 키를 삽입해야 하는지 아니면 교체해야 하는지 여부를 알 수 없으며, 사전에서 특정 키로 값을 찾을 수도 없습니다.
이 요구사항은 Dictionary
의 키 타입에 있는 타입 제약으로 강제됩니다. 해당 제약은 키가 반드시 Swift 기본 라이브러리에 정의된 특수 프로토콜인 Hashable
프로토콜을 따라야만 하도록 지정합니다. Swift의 모든 기본 타입들(String
, Int
, Double
또는 Bool
)은 기본적으로 해시-가능입니다.
사용자 지정 제네릭 타입을 만들 때, 독자적인 타입 제약을 정의할 수 있고, 이 제약들은 제네릭 프로그래밍의 강력함을 제공합니다. Hashable
과 같은 추상 개념은 특정 타입 대신 개념적 특징을 제공합니다.
타입 제약은 타입 패러미터 목록의 일부로 콜론으로 나눠진 클래스나 프로토콜 제약을 타입 패러미터의 이름 뒤에 써서 만듭니다. 타입 제약의 기본적인 문법은 아래에 나타나 있습니다. (제네릭 타입도 문법이 같습니다):
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
// function body goes here
}
위에 있는 가상의 함수는 두 개의 타입 패러미터를 갖고 있습니다. 첫번째 타입 패러미터 T
는 T
가 SomeClass
의 서브클래스가 되도록 요구합니다. 두번째 패러미터 U
는 U
가 SomeProtocol
프로토콜을 준수(conform)하도록 요구합니다.
여기에 findStringIndex
이라 불리는 비-제네릭 함수가 있습니다. 이 함수는 주어진 String
값의 배열에서 주어진 String
값을 찾아냅니다. findStringIndex
함수는 하나의 선택적(optional) Int
값을 반환하는데, 이는 해당 배열에서 찾아낸 처음으로 일치하는 문자열의 인덱스입니다. 일치하는 값이 없으면 nil
을 반환합니다.
func findStringIndex(array: String[], valueToFind: String) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findStringIndex
함수는 문자열 배열 내부에서 문자열 값을 찾기 위해 사용될 수 있습니다:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findStringIndex(strings, "llama") {
println("The index of llama is \(foundIndex)")
}
// prints "The index of llama is 2"
어쨌든, 배열에서 한 값을 찾아내는 원리는 문자열에만 유용한 것이 아닙니다. 문자열에 대한 언급을 어떤 타입 T
로 바꿈으로서 같은 기능을 findIndex
라 불리는 제네릭 함수로 쓸 수 있습니다.
여기 findIndex
라 불리는 제네릭 버전의 findStringIndex
를 예상해 볼 수 있습니다. 반환되는 타입이 그대로 Int?
임에 주목하세요. 함수가 선택적 값이 아닌 선택적 인덱스를 반환하기 때문입니다. 하지만 주의하세요 — 이 함수는 컴파일이 되지 않습니다. 예시 다음에 그 이유를 설명합니다.
func findIndex<T>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
이 함수는 위에 쓰여진대로는 컴파일이 안됩니다. 문제는 동등성[1] 검사(if value == valueToFind
)에 있습니다. Swift의 모든 타입이 동등(equal to) 연산자(==
)로 비교 가능한 것이 아닙니다. 만약 복잡한 데이터 구조를 위해 사용자-지정 클래스나 구조체를 만든다면 Swift는 어떻게 이들의 동등성을 검사할 수 있는지 생각해낼수가 없습니다. 때문에, 모든 가능한 타입 T
에 대해 이러한 코드가 작동하도록 보증하는 것은 불가능하며, 컴파일을 시도하면 적절한 에러가 보고될 것입니다.
[1]: 역주: equality(==
)는 동등성으로 identity(===
)는 정체성으로 번역함. 자세한 것은 "클래스와 구조체의" 하위 항목 클래스는 참조 타입입니다에 있는 "정체성(Identity) 참조.
하지만, 이것이 완전히 불가능하지는 않습니다. Swift 기본 라이브러리는 Equatable
이라 불리는 프로토콜을 정의하는데, 이는 이를 준수하는 타입이 동등 연산자 ==
와 비등 연산자 !=
을 정의하도록 요구합니다. 모든 Swift 기본 타입들은 자동으로 Equatable
프로토콜을 지원합니다.
Equatable
인 모든 타입은 동등 연산자를 지원하므로 안전하게 findIndex
함수와 같이 쓰일 수 있습니다. 함수를 정의할 때 이 사실을 표현하기 위해 타입 패러미터의 일부러 Equatable
제약을 써야 합니다.
func findIndex<T: Equatable>(array: T[], valueToFind: T) -> Int? {
for (index, value) in enumerate(array) {
if value == valueToFind {
return index
}
}
return nil
}
findIndex
의 단일 타입 패러미터는 T: Equatable
로 쓰여 있는데, 이는 "Equatable
프로토콜을 준수하는 어떤 타입 T
"를 의미합니다.
findIndex
함수는 이제 제대로 컴파일되며, Double
이나 String
같이 Equatable
을 준수하는 어떤 타입이든 사용할 수 있습니다.
let doubleIndex = findIndex([3.14159, 0.1, 0.25], 9.3)
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(["Mike", "Malcolm", "Andrea"], "Andrea")
// stringIndex is an optional Int containing a value of 2
프로토콜을 정의할 때, 하나 이상의 연관 타입(associated types)을 정의하는 것이 유용할 때가 있습니다. 연관 타입은 프로토콜의 일부로 사용되는 placeholder 이름(또는 별칭(alias))을 제공합니다. 해당 연관 타입은 프로토콜이 실제로 채용(adopt)될때까지 특정되지 않습니다. 연관 타입은 typealias
키워드로 지정됩니다.
여기에 ItemType
이라는 연관 타입을 선언(declare)하는 Container
라는 프로토콜 예시가 있습니다.
protocol Container {
typealias ItemType
mutating func append(item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container
프로토콜은 모든 컨테이너가 제공해야 하는 세 자기 능력들을 정의합니다.
append
메서드로 새 아이템을 추가할 수 있어야 한다.Int
값을 반환하는 count
속성으로 컨테이너에 있는 아이템의 수를 셀 수 있어야 한다.Int
인덱스 값을 사용하는 첨자(subscript)로 컨테이너 안의 개별 아이템을 얻을 수 있어야 한다.이 프로토콜은 아이템들이 컨테이너 내에 저장되는 방식이나, 아이템의 타입을 지정하지 않습니다. 이 프로토콜은 단지 어떤 타입이든 Container
로 취급되기 위해 제공해야 하는 세 가지 요소를 정의할 뿐입니다. 이를 준수하는 타입은 이 세 가지 요구사항을 만족하는 한 추가적인 기능을 제공할 수 있습니다.
Container
프로토콜을 준수하는 타입은 반드시 그들이 저장하는 값의 타입을 지정할 수 있어야 합니다. 특히, 바른 타입의 아이템들만이 컨테이너에 추가되고 첨자로 반환되도록 보증하는 것이 중요합니다.
이러한 요구사항들을 정의하기 위해 Container
프로토콜은 특정 컨테이너 타입에 대한 정보 없이도 컨테이너가 보유할 요소들의 타입을 아는 것이 중요합니다. Container
프로토콜은 append
메서드로 들어온 모든 값이 컨테이너의 요소 타입와 같은 타입이고, 첨자로 반환될 값도 컨테이너의 요소 타입과 같은 타입이라는 것을 지정해야 합니다.
이를 위해, Container
프로토콜은 ItemType
이라는 연관 타입을 선언합니다. 이는 typealias ItemType
로 쓰여집니다. 프로토콜은 ItemType
이 어느 타입에 대한 별칭(alias)인지는 정의하지 않습니다 — 그 정보는 준수하는(conforming) 타입이 제공해야 합니다. 그럼에도 불구하고, ItemType
별칭은 모든 Container
에 기대되는 행동양식을 강제하기 위해 Container
안에 있는 아이템들을 참조할 수 있는 방법을 제공하고, append
메서드와 첨자에 사용되는 타입을 정의합니다.
여기에 예전에 만든 비-제네릭 버전의 IntStack
타입이 Container
프로토콜을 준수하는 버전이 있습니다.
struct IntStack: Container {
// original IntStack implementation
var items = Int[]()
mutating func push(item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
typealias ItemType = Int
mutating func append(item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack
타입은 Container
프로토콜의 모든 세 가지 요구사항을 구현하며, 이를 위해 개별적으로 IntStack
타입의 기존 기능들을 래핑합니다.
더 나아가, IntStack
는 이 Container
구현에서 적절한 ItemType
은 Int
타입이라는 것을 지정합니다. 이 Container
구현에서 typealias ItemType = Int
라는 정의는 ItemType
라는 추상 타입(abstract type)을 Int
라는 구체 타입(concrete type)으로 바꾸어 줍니다.
Swift의 타입 추론 기능 덕분에, 실제로는 ItemType
라는 구체 타입이 Int
타입이라는 것을 IntStack
정의 일부로 선언할 필요조차 없습니다. IntStack
이 Container
프로토콜의 모든 요구사항을 준수하므로(conforms), Swift는 그냥 append
메서드의 item
패러미터와 첨자(subscript) 리턴 타입을 살펴보는 것만으로도 적절한 ItemType
을 추정할 수 있습니다. 실제로, typealias ItemType = Int
줄을 위의 코드에서 지우더라도 모든것이 제대로 작동하는데, ItemType
이 무슨 타입인지가 명백하기 때문입니다.
또한, 제네릭 Stack
타입이 Container
프로토콜을 준수하도록 만들수도 있습니다.
struct Stack<T>: Container {
// original Stack<T> implementation
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(item: T) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> T {
return items[i]
}
}
이번에는, placeholder 타입 패러미터 T
가 append
메서드의 item
패러미터와 리턴, 그리고 첨자 타입으로 쓰였습니다. Swift는 T
가 이 특정 컨테이너에서 ItemType
에 적절한 타입임을 추정할 수 있습니다.
Adding Protocol Conformance with an Extension에 설명된대로 기존 타입에 특정 프로토콜을 준수(conformance)하도록 추가할 수 있습니다. 이는 연관 타입을 가진 프로토콜도 포함합니다.
Swift의 Array
타입은 이미 append
메서드와 count
속성, 그리고 Int
첨자를 제공합니다. 이 세 가지 능력들(capabilities)은 Container
프로토콜의 요구사항을 만족합니다. 이는 그냥 Array
가 Container
를 채용(adopt)한다고 선언하는 것만으로도 준수(conform)하게 됨을 의미합니다. 이것은 Declaring Protocol Adoption with an Extension에 설명된대로 빈 확장(extension)을 사용함으로써 가능합니다.
extension Array: Container {}
배열의 기존 append
메서드와 첨자는 Swift가 적절한 ItemType
타입을 추정할 수 있도록 해줍니다. 위에 있는 Stack
타입같이 말이죠. 이 확장을 정의한 후에는 Array
를 Container
로 사용할 수 있습니다.
타입 제약에 설명된 타입 제약은 제네릭 함수나 타입에 사용된 타입 패러미터에 요구사항을 정의하게 해줍니다.
연관 타입에 이런 제약을 정의할 수 있다면 쓸모가 많을겁니다. 이는 타입 패러미터 목록의 일부로 where절(where clauses)을 정의하는 것으로 가능합니다. Where절은 연관 타입이 특정 프로토콜을 준수하거나 해당 특정 타입 패러미터가 연관 타입과 같도록 요구할 수 있게 해줍니다. Where절을 만들기 위해서는, where
키워드를 패러미터 타입 목록 바로 뒤에 쓰고, 하나 이상의 연관 타입 제약을 쓰고, 타입과 연관 타입간의 일치 관계를 하나 이상 쓰면 됩니다.
다음의 예시는 두 Container
인스턴스들이 동등한 아이템을 같은 순서로 보유하는지 여부를 검사하는 allItemsMatch
이라는 제네릭 함수를 정의합니다. 이 함수는 모든 아이템이 동등하면 true
를, 아니라면 false
를 반환합니다.
두 컨테이너들은 같은 타입일 필요가 없지만 (물로, 같아도 됩니다) 보유하는 아이템들의 타입은 같아야 합니다. 이 요구사항은 타입 제약과 where절의 조합으로 표현됩니다.
func allItemsMatch<
C1: Container, C2: Container
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable>
(someContainer: C1, anotherContainer: C2) -> Bool {
// check that both containers contain the same number of items
if someContainer.count != anotherContainer.count {
return false
}
// check each pair of items to see if they are equivalent
for i in 0..someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// all items match, so return true
return true
}
이 함수는 someContainer
와 anotherContainer
라 불리는 두 개의 인수를 받습니다. someContainer
인수는 C1
의 타입이고, anotherContainer
인수는 C2
의 타입입니다. C1
과 C2
모두 함수가 호출될때 밝혀질 두 컨테이너 타입을 위한 placeholder 타입 패러미터입니다. 함수의 타입 패러미터 리스트는 두 타입 패러미터 목록에 다음 요구사항들을 추가합니다.
C1
은 반드시 Container
프로토콜을 준수해야 합니다. (예: C1: Container
)C2
도 반드시 Container
프로토콜을 준수해야 합니다. (예: C2: Container
)C1
의 ItemType
은 C2
의 ItemType
과 같아야 합니다. (예: C1.ItemType == C2.ItemType
)C1
의 ItemType
은 Equatable
프로토콜을 준수해야 합니다. (예: C1.ItemType: Equatable
) 세번째와 네번째 요구사항들은 where절의 일부로 정의되었고, where
키워드 뒤에 함수의 타입 패러미터 목록의 일부로 따라옵니다.
이 요구사항들은:
someContainer
는 C1
의 컨테이너 타입입니다.anotherContainer
는 C2
의 컨테이너 타입입니다.someContainer
와 anotherContainer
는 같은 타입의 아이템들을 가집니다.someContainer
안에 있는 아이템들은 비등 연산자(!=
)를 사용해 서로 다른지 여부를 검사할 수 있습니다.를 의미합니다. 세번째와 네번째 요구사항의 조합은 anotherContainer
의 아이템들 또한 !=
연산자로 검사될 수 있음을 의미하는데, 왜냐면 그것들이 someContainer
의 아이템 타입과 동일하기 때문입니다.
이 요구사항들은 두 개의 컨테이너 타입이 다른 경우에도 allItemsMatch
함수가 두 컨테이너들을 비교할 수 있게 해줍니다.
allItemsMatch
함수는 컨테이너들이 같은 수의 아이템을 갖고 있는지부터 검사하는데, 아이템 수가 다르면 같다고 볼 수가 없기 때문입니다. 이 경우 함수는 false
를 반환합니다.
이 검사가 끝난 후, 이 함수는 someContainer
안에 있는 모든 아이템들을 for
-in
루프와 반폐쇄 범위 연산자(..
)로 반복(iterate)합니다. 모든 아이템들에 대해, 이 함수는 someContainer
의 아이템이 anotherContainer
에 있는 대응 아이템과의 같은지(not equal) 여부를 검사합니다. 만약 둘이 다르면 두 컨테이너는 다른 것이고, 함수는 false
를 반환합니다.
만약 저 루프가 다른 아이템을 찾지 못하고 종료된다면 두 컨테이너는 같은 것이고 함수는 true
를 반환합니다.
allItemsMatch
함수의 실제 모습은 이렇습니다.
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
println("All items match.")
} else {
println("Not all items match.")
}
// prints "All items match."
위의 예시는 String
값들을 저장하기 위한 Stack
인스턴스를 만들, 새 개의 문자열을 스택에 추가(push)합니다. 위 예시는 스택과 동일한 세 문자열을 포함하는 리터럴로 초기화된 Array
인스턴스도 만듭니다. 스택과 배열은 다른 타입이지만, 이들은 둘 다 Container
프로토콜을 준수하며, 같은 타입의 아이템들을 가집니다. 그러므로 이 두 컨테이너들을 인수로 사용해 allItemsMatch
함수를 호출할 수 있습니다. 위의 예시에서는 allItemsMatch
함수가 두 컨테이너 내의 모든 아이템들이 같다고 정확하게 보고합니다.
Translator : 심상진 (dyanos@gmail.com)
기본 연산자 항목에서 설명했던 연산자들에 더하여, Swift는 훨씬 다양한 방법으로 값을 다루는 몇 개의 고급 연산자들을 제공합니다. 이들은 당신이 C와 Objective-C에서부터 친근하게 여겼던 비트를 다루는 연산자 모두를 포함합니다.
C에서의 산술 연산자들과는 다르게, Swift에서의 산술 연산자들은 기본적으로 오버플로우(Overflow)가 일어나지 않습니다. 오버플로우 동작(Overflow behavior)은 오류로써 잡히고 보고됩니다. 오버플로우 동작을 허용하기 위해서, 오버플로우를 기본으로 하는 산술 연산들 중에 Swift의 두번째 집합을 사용해야 합니다. 예를 들어, 오버플로우 덧셈(overflow addition, &+)이 그러한 집합에 속합니다. 모든 오버플로우 연산자들은 엠퍼샌드(ampersand, &)를 가지고 시작합니다.
당신이 당신 소유의 구조체들과 클래스, 그리고 열거자들을 선언할때, 이들 사용자 정의 타입들에 대해서 표준 Swift 연산자들의 독자적인 구현들(own implementations)을 제공하는데 유용할 수 있습니다. Swift는 이들 연산자들의 맞춤형(tailored) 구현들을 제공하고 그들의 행동이 당신이 만든 각각의 타입에 대해서 무엇을 해야 할지를 정확하게 결정하기 쉽게 만듭니다.
당신은 연산자들을 재정의하는데 아무런 제한이 없습니다. Swift는 당신에게 당신 자신의 맞춤형 중위(infix), 전위(prefix), 후위(postfix) 그리고 할당 연산자들을 정의하는데 자유를 줍니다. 그리고 그것들의 우선순위와 결합순위 역시 자유롭게 정의가 가능합니다. 이들 연산자들은 마치 이미 선언된 연산자들처럼 당신의 코드 안에서 사용되고 적용될 수 있으며, 당신은 당신이 정의한 맞춤형 연산자들을 지원하도록 이미 존재하는 타입들조차 확장할 수 있습니다.
비트 연산자들은 당신에게 하나의 데이터 구조체 안에 있는 개개의 가공되지 않은 데이터 비트들(raw data bits)을 다루는 것을 허용합니다. 그들은 종종 그래픽 프로그래밍과 디바이스 드라이버 제작과 같은 저수준 프로그래밍에 사용됩니다. 또한 비트 연산자들은 당신이 외부의 입력들(external source)로부터 가져오는 가공되지 않은 데이터(raw data)를 가지고 작업할때 유용합니다. 예를 들어, 사용자 정의 프로토콜을 이용한 통신에서 데이터의 부호화(encoding)와 복호화(decoding)과 같은 것들이 그것입니다.
Swift는 C에서 발견되는 모든 비트 연산자들을 지원합니다. 이는 아래에서 좀더 자세히 설명드리겠습니다.
비트 NOT 연산자(~)는 다음과 같이 숫자의 모든 비트들을 뒤집습니다.(invert)
비트 NOT 연산자는 전위연산자입니다. 그리고 공백없이, 연산하는 값 바로 앞에 나타납니다.
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // equals 11110000
UInt8 정수들은 8개의 비트를 가지며, 0에서부터 255까지의 임의의 값을 저장할 수 있습니다. 이 예에서는 UInt8 정수 변수를, 최초의 4개 비트는 0으로, 나머지 4개비트는 1로 설정한, 이진 값 00001111을 가지도록 초기화합니다. 이것은 십진수 15와 동일한 것입니다.
다음 줄에서, 비트 NOT 연산자는 invertedBits라 불리우는 새로운 상수를 생성하는데 사용합니다. 이것은 initialBits와 동일하지만 모든 비트들이 뒤집어져 있습니다. 다시말해, 이때 initialBit의 비트들중에 0은 1이되고, 1은 0이 됩니다. "그러므로" invertedBits의 값은 11110000이 됩니다. 이것은 부호없는 십진수 240과 동일합니다.
비트 AND 연산자(&)는 두 숫자의 비트들을 결합합니다. 다음과 같이 동일 위치에 있는 비트들이 양쪽 입력 숫자들에 대해서 둘 다 1이면, 결과 값의 동일 위치에 있는 비트 역시 1로 설정되는 새로운 숫자를 돌려받습니다.(""""좀더 명확하게 이해되도록 수정해야 할 필요가 있음"""")
아래의 예에서, firstSixBits변수와 lastSixBits양쪽의 값들은 4개의 중간 비트가 1로 되어있습니다. 비트 AND 연산자는 그들을 부호 없는 십진수 60과 동일한 숫자인 00111100로 만들도록 조합합니다.
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // equals 00111100
비트 OR 연산자(|)는 두 수의 비트들을 비교합니다. 만일 다음처럼 입력 수들 중에 어떤 하나가 비트 1이면, 연산자는 해당 위치의 비트가 1로 설정된 새로운 수를 돌려줍니다.
아래의 예제에서, someBits와 moreBits의 값은 서로 다른 위치에 비트 1을 가지고 있습니다. 비트 OR 연산자는 그들을 부호 없는 십진수 254와 동일한 숫자인 11111110으로 만들어지도록 조합합니다.
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // equals 11111110
비트 XOR 연산자 또는 배타적(exclusive) OR 연산자 (^)는 두 수의 비트들을 비교합니다. 연산자는 다음과 같이 동일 위치에 두 입력 비트들이 서로 다른 값을 가지면 1로 같은 값을 가지면 0으로 설정된 새로운 수를 돌려받습니다.
아래 예에서, firstBits와 otherBits 각각의 값들은 하나의 위치에서 1로 설정된 하지만 다른 변수에서는 그렇지 않은 비트를 가집니다. 비트 XOR 연산자는 그것들의 출력 값에서 이들 비트들의 양쪽을 1로 설정합니다. firstBits와 otherBits에서 모든 다른 비트들은 같으며, 이것은 다음과 같이 출력 값에서 0으로 나타납니다.
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // equals 00010001
비트 왼쪽 이동 연산자(<<)와 비트 오른쪽 이동 연산자(>>)는 아래 정의된 규칙에 따라서, 특정 수의 위치(a certain number of places)로 모든 비트들을 왼쪽 또는 오른쪽으로 이동시킵니다.
비트 왼쪽 그리고 오른쪽 쉬프트는 2의 인수로 정수에 곱한 것과 나눈 것의 효과를 가집니다. 왼쪽으로 한 자리만큼 정수의 비트들을 이동하는 것은 값을 두 배로 하는 것과 같은 효과를 나타냅니다. 마찬가지로 오른쪽으로 이동하는 것은 2로 나누는 것과 동일한 효과를 가집니다.
부호 없는 정수의 비트 쉬프트는 다음처럼 합니다.
이 접근은 논리적 쉬프트로써 알려져 있습니다.
아래의 그림은 11111111<<1
의 결과를 보여줍니다.(여기서는 왼쪽으로 1만큼 이동하는 것을 말합니다.) 그리고 11111111>>1
(이것은 오른쪽으로 1만큼 이동하는 것을 말합니다.) 여기서 파란색 비트들은 쉬프트된 비트들을 말하며, 회색 비트들은 버려진 것을 말합니다. 그리고 오랜지 색의 0은 삽입된 것을 말합니다.
여기서는 Swift 코드 안에서 어떻게 비트 쉬프트를 하는지를 다음의 실제 코드로 보여줍니다.
let shiftBits: UInt8 = 4 // 00000100 in binary
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001
당신은 다음과 같이 다른 데이터 타입들 안에 있는 값들을 부호화하기 위해서 그리고 복호화하기 위해서 비트 쉬프트를 사용할 수 있습니다.
let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16 // redComponent is 0xCC, or 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent is 0x66, or 102
let blueComponent = pink & 0x0000FF // blueComponent is 0x99, or 153
이 예제는 핑크색에 대한 Cascading Style Sheets 색 값을 저장하기 위해 pink로 불리는 UInt32 타입의 상수를 선언합니다. CSS 컬러 값 #CC6699는 Swift의 16진수 표현으로 0xCC6699가 됩니다. 이 색깔은 비트 AND 연산자(&)와 비트 오른쪽 쉬프트 연산자(>>)를 사용하여 빨간색 (CC), 녹색(66), 파란색 (99) 요소들로 나눌 수 있습니다..
빨간색 요소는 숫자 0xCC6699와 0xFF0000사이에 비트 AND 연산을 수행함으로써 얻어집니다. 6699를 무시하기 위해서 그리고 결과에서 0xCC0000를 남기기 위해서, 0xFF0000에서의 0은 0xCC6699의 두 번째와 세 번째 바이트를 효과적으로 가려줍니다.(mask)
그때 이 수는 오른쪽으로 16칸 쉬프트(>>16)합니다. 16진수에서의 두 자리는 2진수의 8비트와 같습니다, 그래서 오른쪽으로 16칸 쉬프트은 0xCC0000를 0x0000CC로 변환할 것 입니다. 이것은 10진수 204인 0xCC와 같습니다.
비슷하게, 녹색 요소는 출력으로써 0x006600을 주는 0xCC6699와 0x00FF00사이에 비트 AND 연산을 수행함으로써 얻어집니다. 이 출력은 오른쪽으로 8칸 쉬프트되고, 10진수로 102에 해당하는 0x66의 값을 줍니다.
마지막으로, 파란색 요소는 출력으로 0x000099를 주는 0xCC6699와 0x0000FF사이의 비트 AND 연산을 수행함으로써 얻어집니다. 여기서는 오른쪽으로의 쉬프트가 필요 없습니다. 이미 0x000099는 10진수로 153에 해당하는 0x99와 동일하기 때문입니다.
부호 있는 정수에 대해서 쉬프트를 하는 것은 부호 없는 정수 때보다 더 복잡합니다. 이는 부호 있는 정수를 이진수로 표현하는 방식 때문입니다. (아래 예들은 간단함을 위해 8비트 부호 있는 정수들을 기본으로 하여 진행됩니다. 그러나 어떠한 크기의 부호 있는 정수에도 앞으로 나올 원칙을 적용할 수 있습니다.)
부호 있는 정수들의 (부호 비트로 알려진) 첫 번째 비트는 그 정수가 양의 정수인지 음의 정수인지를 나타내는데 사용합니다. 부호비트가 0이면 양수를, 부호비트가 1이면 음수를 의미합니다.
값 비트로 알려진 (부호 비트를 제외하고) 남은 비트들은 실제 값을 저장합니다. 양의 정수는 정확하게 부호 없는 정수에 대해서 하는 것과 같은 방법인 0부터 위쪽으로 계산하는 방법(counting upwards from 0)으로 저장합니다. 여기서는 어떻게 Int8안에서 숫자 4를 표현하는지 보여줍니다.
부호 비트가 0(즉, 양수)이고, 7개의 값 비트들은 단지 이진 표현으로 쓰여진 숫자 4를 의미합니다.
그렇지만 음수는 다르게 저장됩니다. 2의 n승에서 그들의 절대값을 뺌으로써 저장됩니다. 이때 n은 값 비트의 수를 의미합니다. 8비트 수는 7개의 값 비트를 가집니다. 그래서 이것은 2의 7승 또는 128을 의미합니다.
여기서는 어떻게 Int8에서 -4를 표현하는지 보여줍니다.
이번에는, 부호 비트가 1(즉, 음수)이고, 7개의 비트는 이진 값으로 (128 - 4인) 124를 가집니다.
음수에 대한 부호화 방법은 2의 보수 표현법으로써 알려져 있습니다. 이것은 이상한 방법처럼 보이지만, 이러한 방법은 몇 가지 이득을 가집니다.
첫 번째, 다음과 같이 (부호 비트를 포함하는) 모든 8개의 비트들에 대해서 표준 이진 덧셈을 하고, 8비트에 적합하지 않은 어떤 것도 버릴 필요 없이 간단하게 -1을 -4에 더할 수 있습니다.
두 번째, 2의 보수 표현은 당신에게 양수에서와 같이 음수의 비트들을 왼쪽 또는 오른쪽으로 이동시키고, 여전히 왼쪽 이동에 대해서 그들을 배가하거나 오른쪽 쉬프트 함으로써 반분되도록 합니다. 이것을 이루기 위해서, 부호 있는 정수를 오른쪽으로 이동시킬 때 다음의 추가적인 규칙들이 적용됩니다.
당신이 오른쪽으로 부호 있는 정수를 이동시킬 때, 부호 없는 정수에서와 같은 규칙들을 적용하면 됩니다만 부호와 함께 왼쪽에 있는 임의의 빈 비트들을 0과는 다른 것으로 채워야 합니다.
이러한 행동은 부호 있는 정수들이 오른쪽으로 쉬프트 후에도 같은 부호를 가지는 것을 확실히 하기 위해서 입니다. 그리고 이러한 행동은 산술 쉬프트(arithmetic shift)이라고 알려져 있습니다.
양수와 음수가 저장되는 특별한 방식 때문에, 그들 중에 하나를 오른쪽으로 쉬프트하는 것은 그들의 값을 0에 더 가깝게 쉬프트 시킨다는 것을 의미합니다. 이렇게 쉬프트하는 동안 부호 비트를 동일하게 유지하는 것은 그들의 값을 0에 더 가깝게 쉬프트하는 동안에도 그 값을 음수로 남아있게 한다는 것을 의미합니다.
만일 당신이 해당 타입의 변수가 가질 수 없는 값을 정수 상수 또는 변수에 숫자의 대입을 시도한다면, 기본적으로 Swift는 유효하지 않은 값이 생성되기를 허락하기 보다는 오류를 보고 합니다. 이 행동은 당신이 너무 크거나 너무 작은 숫자들을 가지고 작업할 때 추가적인 안전함(extra safety)을 당신에게 제공합니다.
예를 들어, Int16 정수 타입은 -32768부터 32767까지의 임의의 부호 있는 정수를 가지고 있을 수 있습니다. UInt16 상수 또는 변수에 이 범위를 벗어나는 수를 설정하려고 노력하는 것은 오류를 일으킵니다.
var potentialOverflow = Int16.max
// potentialOverflow는 3276과 동일합니다. 이것은 Int16이 가질 수 있는 가장 큰 값입니다.
potentialOverflow += 1
// 이것은 오류를 발생합니다.
값이 너무 크거나 너무 작을 때 에러 핸들링을 제공하는 것은 경계 값 조건과 관련된 코딩을 할 때 훨씬 더 많은 유연성을 당신에게 줍니다.
그렇지만, 당신이 사용 가능한 비트들의 수를 일부로 줄이기 위해서 오버플로우 조건을 특별히 원할 때, 당신은 오류를 일으키는 것보다 다음의 행동으로 이를 수행할 수 있습니다. Swift는 정수 계산에 대해서 오버플로우 동작을 수행할 수 있는 다섯 가지의 오버플로우 연산자들을 제공합니다. 이들 연산자들 모두는 앰퍼센트(&)를 가지고 시작합니다.
여기서는 오버플로우 덧셈 연산자(&+)를 사용하여, 부호 없는 값이 오버플로우가 허용될 때 무슨 일이 일어나는지에 대한 예를 보여줍니다.
var willOverflow = UInt8.max
// willOverflow는 255와 동일합니다. 이것은 UInt8이 가질 수 있는 최대 값입니다.
willOverflow = willOverflow &+ 1
// willOverflow는 지금부터 0과 동일합니다.
변수 willOverflow는 UInt8이 가질 수 있는 최대 값(즉, 255 또는 이진수로 11111111)으로 초기화되어 있습니다. 그때 오버플로우 덧셈 연산자(&+)를 사용하여 1을 증가시킵니다. 이것은 그것들의 이진 표현을 UInt8의 크기를 넘도록 밀어내는데, 이것은 아래 그림에서 보여지듯이 UInt8이 가질 수 있는 값의 범위를 넘어서게 되고 오버플로우를 발생시킵니다. 오버플로우 덧셈 이후로 UInt8의 범위 안에 남아있는 값은 00000000 또는 0입니다.
숫자들은 또한 너무 작아서 그들 타입의 최대 범위에 안 맞게 될 수도 있습니다. 여기에 예제가 있습니다.
UInt8가 유지할 수 있는 가장 작은 수는 0(즉, 8비트 이진 형태에서는 00000000이 됩니다.)입니다. 만일 당신이 오버플로우 뺄셈 연산자를 사용하여 00000000으로부터 1을 뺀다면, 그 수는 이진수 11111111 또는 십진수 255으로 꺼꾸로 넘칠 것 입니다.
다음은 Swift코드 에서 어떻게 보이는 지를 나타냅니다.
var willUnderflow = UInt8.min
// willUnderflow는 UInt8이 유지할 수 있는 가장 작은 값인 0이 됩니다.
willUnderflow = willUnderflow &- 1
// 현재 willUnderflow는 255와 동일합니다.
유사한 언더플로는 부호 있는 정수에서 발생됩니다. 부호 있는 정수들에 대한 모든 뺄셈은 직접적인 이진 뺄셈으로써 수행됩니다. 이는 뺼셈을 하고 있는 숫자의 부분으로써 포함되어 있는 부호비트도 함께이며, 비트 왼쪽 그리고 오른쪽 연산자들에서 설명한 것과 같습니다. Int8이 가질 수 있는 가장 작은 값은 -128입니다. -128은 이진수로 10000000로 나타납니다. 오버플로우 연산자를 가지고 이 이진 수로부터 1을 빼는 것은 01111111의 이진 수를 줍니다. 이것은 부호비트를 뒤집고 양수 127을 줍니다. 이는 Int8이 가질 수 있는 가장 큰 양의 수입니다.
다음은 Swift코드에서의 표현입니다.
var signedUnderflow = Int8.min
// signedUnderflow는 -128과 같습니다. 이는 Int8이 가질 수 있는 가장 작은 값입니다.
signedUnderflow = signedUnderflow &- 1
// signedUnderflow는 지금 127과 같습니다.
위에 설명된 오버플로우와 언더플로의 행동의 마지막 결과는 부호 있는 그리고 부호 없는 정수 양쪽에 대해서, 항상 오버플로우가 가장 크게 유효한 정수 값으로부터 가장 작은 것으로 반복되며, 언더플로는 가장 작은 값으로부터 가장 큰 값으로 반복됩니다.
0으로 숫자를 나는 것(i/0) 또는 0으로 나머지를 계산하기(i%0)를 시도하는 것은 오류를 발생시킵니다.
1:let x = 1 2:let y = x / 0
그렇지만 이들 연산자들(&/와 &%)의 오버플로우 버전들은 당신이 만일 0으로 나누면 0의 값을 돌려줍니다.
1:let x = 1 2:let y = x &/ 0 3:// y는 0입니다.
연산자 우선순위는 다른 것보다 더 높은 우선 순위를 몇몇 연산자에게 줍니다: 이들 연산자들은 첫 번째로 계산됩니다.
연산자 결합순위는 같은 우선순위의 연산자들이 어떻게 함께 그룹화되는지 또는 왼쪽으로부터 그룹화되는지, 아니면 오른쪽으로부터 그룹화되는지를 정의합니다. "그들이 그들의 오른쪽으로 그 표현(expression)과 관련 있다는 의미 또는 "그들은 그들의 오른쪽으로 그 표현과 관련 있다는 의미로써 그것을 생각해보세요.(해석이 애매함...)
복합 표현이 계산될 곳에서 계산 순서로 계산할 때 각각의 연산자의 우선순위와 결합순위를 고려하는 것은 중요합니다. 다음은 예입니다. 왜 다음에 표현이 4일까요?
2 + 3 * 4 % 5
// 이것은 4와 동일합니다.
엄격하게 왼쪽에서부터 오른쪽으로 얻어질 때, 당신은 이것을 다음처럼 읽기를 기대할지도 모릅니다.
그렇지만, 실제 답은 0이 아니라 4입니다. 더 높은 우선순위의 연산자들은 낮은 우선순위를 가진 연산자보다 먼저 계산됩니다. Swift에서는, C에서와 같이, 곱셈 연산자(*)와 나머지 연산자(%)는 덧셈 연산자(+)보다 더 높은 우선순위를 가집니다. 결과적으로, 그들은 덧셈이 고려되기 전에 양쪽 다 계산됩니다.
그렇지만, 곱셈과 나머지 연산자는 서로에 대해서 같은 우선순위를 가집니다. 정확한 계산 순위를 얻기 위해서는, 당신은 그들의 결합순위 또한 고려할 필요가 있습니다. 곱셈과 나눗셈 양쪽은 그들의 왼쪽에서부터 결합시킵니다. 그들의 오른쪽에서 시작하는 표현의 이들 부분들 주변에 내포된 괄호를 더함으로써 이것을 생각해보세요.
2 + ((3 * 4) % 5)
(3 * 4)는 12입니다. 그래서 이것은 다음으로 표현됩니다.
2 + (12 % 5)
(12 % 5)는 2입니다. 역시 이것은 다음으로 표현됩니다.
2 + 2
이것의 계산은 4를 답으로써 이야기합니다.
Swift에서 연산자 우선순위와 결합순위의 완벽한 목록에 대해서는 "Expressions" 항목을 보세요.
참고 Swift의 연산자 우선순위와 결합순위 규칙은 C와 Objective-C에서 발견되는 것보다 더 간단하고 더 쉽게 예측될 수 있습니다. 하지만, 이것은 그것들이 C를 기본으로 하는 언어들에서와 완전히 같지 않다는 것을 의미합니다. 여전히 연산자들 간의 상호작용이 이미 존재하는 코드를 Swift코드로 포팅할때 당신이 의도하는 방식으로 동작하는지에 대해서 확신을 가지고 주의 깊게 적용해야 합니다.
클래스와 구조체는 이미 존재하는 연산자들에 대해서 그들 자신의 구현을 제공할 수 있습니다. 이것은 이미 존재하는 연산자들을 오버로딩하는 것으로 알려져 있습니다.
아래의 예는 사용자 정의 구조에 대해서 산술 덧셈 연산자(+)를 어떻게 구현할 수 있는지를 보여줍니다. 산술 덧셈 연산자는 두 개의 대상에서 동작하기 때문에 2항 연산자이며, 그것이 이들 두 개의 대상 사이에서 나타나기 때문에 중간연산자라고 불릴 수 있습니다.
예는 2차원 위치 벡터 (x, y)에 대한 Vector2D 구조체를 정의합니다. 여기서 Vector2D 구조체의 인스턴스들을 함께 더하기 위한 연산자 함수의 정의가 뒤따릅니다.
struct Vector2D {
var x = 0.0, y = 0.0
}
@infix func + (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
연산자 함수는 '+'이라고 불리는 전역 함수로써 선언됩니다. 이 함수는 두 개의 입력 파라메터로 Vector2D 타입을 가지며, 하나의 단일 출력 값을 돌려줍니다. 이때 출력 값의 타입은 Vector2D입니다. 당신은 @infix라는 속성을 연산자 함수 선언할 때 'func' 키워드 앞에 씀으로써 중간 연산자를 구현하는 것이 됩니다.
이 구현에서, 입력 파라메터들은 '+' 연산자의 왼쪽과 오른쪽에 있는 타깃들을 Vector2D 인스턴스로 표현하는 left와 right라는 변수로 이름 지어져 있습니다. 이 함수는 새로운 Vector2D 인스턴스를 돌려줍니다. 새로운 인스턴스의 x와 y는 더해지는 두 개의 Vector2D 인스턴스들로부터 x속성들의 합과 y속성들의 합으로써 초기화 됩니다.
함수는 Vector2D 구조체상의 하나의 함수로써가 아닌, 전역적으로 정의됩니다. 그것은 존재하는 Vector2D 인스턴스들 사이의 중간 연산자로써 사용되기 위해서 입니다.
let vector = Vector2D(x: 3.0, y: 1.0)
let anotherVector = Vector2D(x: 2.0, y: 4.0)
let combinedVector = vector + anotherVector
// combinedVector는 (5.0, 5.0)의 값을 가진 Vector2D 구조체의 인스턴스입니다.
이 예는 아래의 그림처럼 두 벡터 (3.0, 1.0)과 (2.0, 4.0)을 벡터 (5.0, 5.0)으로 만들기 위해서 더 합니다.
위에서 보여준 예는 2항 중간 연산자의 사용자 정의 구현을 설명한 것 입니다. 클래스와 구조체들은 표준 단항 연산자들의 구현을 제공해줄 수 있습니다. 단항 연산자들은 단일 타깃에 대해서 동작합니다. 만일 그것들이 그들의 타깃보다 앞서서 나타난다면(예를 들어 -a와 같은) 전위 연산자이고, 반대로 그들의 타깃 뒤에서 나타난다면(i++과 같은) 후위 연산자라고 말합니다.
당신은 연산자 함수를 선언할 때 'func' 키워드 앞에 '@prefix' 또는 '@postfix' 속성을 사용함으로써 전위 또는 후위 단항 연산자를 구현합니다.
@prefix func - (vector: Vector2D) -> Vector2D {
return Vector2D(x: -vector.x, y: -vector.y)
}
위의 예는 Vector2D 인스턴스에 대해서 단항 뺄셈 연산자(-a)를 구현합니다. 단항 뺄셈 연산자는 전위 연산자이고, 그래서 이 함수는 '@prefix'속성으로 전위연산자임을 알려주어야 합니다.
간단한 수치 값들에 대해서, 단항 뺄셈 연산자는 양수를, 부호를 뒤집을 때 같아지는 음수로 변환합니다. Vector2D에 대한 동일한 구현은 x와 y속성들 양쪽에 이 동작을 수행합니다.
let positive = Vector2D(x: 3.0, y: 4.0)
let negative = -positive
// 음수는 (-3.0, -4.0)의 값을 가지는 Vector2D 인스턴스가 됩니다.
let alsoPositive = -negative
// alsoPositive는 (3.0, 4.0)의 값을 가지는 Vector2D 인스턴스가 됩니다.
복합 할당 연산자들은 다른 동작에 할당(=) 연산자를 결합한 것 입니다. 예를 들어, 덧셈 할당 연산자(+=)는 하나의 동작 안에 덧셈과 할당 연산을 합친 것 입니다. 복합 할당 연산자를 구현하는 연산자 함수는 '@assignment' 속성을 기술함으로써 결합 할당 연산자임을 알려주어야 합니다. 당신은 또한 복합 할당 연산자들의 왼쪽 입력 파라메터들을 'inout'으로써 표시해야만 합니다. 이것은 파라메터의 값이 연산자 함수 안에서 직접적으로 수정될 것이기 때문입니다.
아래 예는 Vector2D 인스턴스들에 대해서 덧셈 할당 연산자 함수를 구현한 것 입니다.
@assignment func += (inout left: Vector2D, right: Vector2D) {
left = left + right
}
덧셈 연산자는 더 먼저 정의되었기 때문에, 당신은 덧셈 절차를 여기서 다시 구현할 필요가 없습니다. 대신에 덧셈 할당 연산자 함수는 존재하는 덧셈 연산자 함수의 이점을 가져오고, 그것은 왼쪽 값을 오른쪽 값과 더하여 왼쪽 값에 설정하기 위해서 그것을 사용합니다.
var original = Vector2D(x: 1.0, y: 2.0)
let vectorToAdd = Vector2D(x: 3.0, y: 4.0)
original += vectorToAdd
// original은 현재 (4.0, 6.0)의 값을 가집니다.
당신은 '@prefix'또는 '@postfix' 속성 둘 중에 하나를 '@assignment'속성과 함께 결합할 수 있습니다. 이는 Vector2D 인스턴스에 대해서 전위 증가 연산자 (예로 ++a)의 구현에서 사용할 수 있습니다.
@prefix @assignment func ++ (inout vector: Vector2D) -> Vector2D {
vector += Vector2D(x: 1.0, y: 1.0)
return vector
}
위의 전위 증가 연산자 함수는 초기에 정의된 덧셈 할당 연산자의 이득을 취합니다. 그것은 그것이 불려진 곳 상에서 x값과 y값으로 1.0을 가지는 Vector2D를 더합니다. 그리고 결과를 돌려줍니다.
var toIncrement = Vector2D(x: 3.0, y: 4.0)
let afterIncrement = ++toIncrement
// toIncrement는 지금 (4.0, 5.0)의 값을 가집니다.
// afterIncrement는 또한 (4.0, 5.0)의 값을 가집니다.
주목 기본 할당 연산자(=)를 오버로드하는 것은 불가능합니다. 단지 복합 할당 연산자들만이 오버로드됩니다. 비슷하게 3항 조건 연산자(a ? b : c)는 오버로드될 수 없습니다.
사용자 정의 클래스와 구조체들은 동등 연산자들, 즉 "같음(equal to)" 연산자 (==)와 "다름" 연산자(!=)로써 알려져 있는 연산자들의 기본 구현들을 받지 못 합니다. Swift에서는 당신 자신의 사용자 정의 타입에 대해서 "같음"으로 인정될 수 있는 것에 대한 추측하는 것이 불가능합니다. 이것은 "같음"의 정의가 당신의 코드에서 이들 타입들이 수행하는 역할에 의존하기 때문입니다.
사용자가 만든 타입의 동등성 검사를 위한 동등성 연산자를 사용하기 위해서는 다른 중위 연산자들에 대해서와 같이 연산자들의 구현을 제공해야 합니다.
@infix func == (left: Vector2D, right: Vector2D) -> Bool {
return (left.x == right.x) && (left.y == right.y)
}
@infix func != (left: Vector2D, right: Vector2D) -> Bool {
return !(left == right)
}
위의 예는 두 개의 Vector2D 인스턴스가 동등함 값을 가지는지에 대해서 검사하기 위해서 "같음" 연산자(==)를 구현하는 것입니다. Vector2D의 컨텍스트에서 그것은 "같음"을 "양쪽 인스턴스가 같은 x값과 y값들을 가진다"는 의미로써 고려되는 것이 이치에 맞습니다. 그래서 이것은 연산자 구현에 의해서 사용된 논리입니다. 예는 또한 "같지 않음" 연산자(!=)를 구현합니다. 이것은 간단하게 "같음" 연산자의 결과에 역을 돌려줍니다.
당신은 지금 두 개의 Vector2D 인스턴스들이 같은지 아닌지를 검사하는데 이들 연산자들을 사용할 수 있습니다.
let twoThree = Vector2D(x: 2.0, y: 3.0)
let anotherTwoThree = Vector2D(x: 2.0, y: 3.0)
if twoThree == anotherTwoThree {
println("These two vectors are equivalent.")
}
// prints "These two vectors are equivalent."
당신은 Swift에 의해서 제공되는 표준 연산자들뿐만이 아니라 당신 소유의 사용자 정의 연산자들을 선언하고 구현할 수 있습니다. 사용자 정의 연산자들은 문자들 / = - + * % < > ! & | ^ . ~.를 가지고 단지 정의될 수 있습니다.
새로운 연산자들은 연산자 키워드를 사용하여 전역 수준에서 정의되고, 전위, 중위 또는 후위로써 정의될 수 있습니다.
operator prefix +++ {}
위의 예는 '+++'라고 불리는 새로운 전위 연산자를 정의합니다. 이 연산자는 Swift에서 미리 정의된 의미를 가지고 있지 않습니다. 그래서 Vector2D 인스턴스들과 함께 동작하는 특정 컨텍스트 안에서 아래와 같이 의미를 부여는 자신 소유의 사용자 정의 연산자를 선언할 수 있습니다. 이 예제의 목적을 위해서, '+++'를 새로운 "전위 두 배 증가" 연산자로써 다룹니다. 그것은 이전에 정의했던 덧셈 할당 연산자를 통해 그 자신을 그 벡터에 더하므로 써, Vector2D 인스턴스의 x와 y값을 두 배가 증가 시킵니다.
@prefix @assignment func +++ (inout vector: Vector2D) -> Vector2D {
vector += vector
return vector
}
'+++'의 이 구현은 Vector2D에 대해서 '++'의 구현과 매우 비슷합니다. 단지 이 연산자 함수가 Vector2D(1.0, 1.0)을 더하는 것 보다, 벡터를 그 자신에 더한다는 것을 제외하고는 같습니다.
var toBeDoubled = Vector2D(x: 1.0, y: 4.0)
let afterDoubling = +++toBeDoubled
// toBeDoubled는 지금 (2.0, 8.0)의 값들을 가집니다.
// afterDoubling은 또한 (2.0, 8.0)의 값들을 가집니다.
사용자 정의 중위 연산자들 또한 우선순위와 결합순위를 나열할 수 있습니다. 이들 두 개의 문자를 가진 연산자들이 다른 중위 연산자들과 중위 연산자들의 상호작용에 어떻게 영향을 미치는지에 대한 설명을 위해서 'Precedence and Associativity'장을 보세요.
결합순위에 대해서 가능한 조건들은 왼쪽, 오른쪽, 그리고 아무것도 아닌 쪽이 있습니다. 왼쪽 결합 연산자들은 만일 같은 우선순위를 가진 다른 왼쪽 결합 연산자들 옆에 쓰여져 있다면 왼쪽으로 결합합니다. 유사하게, 오른쪽 결합 연산자들은 같은 우선순위의 다른 오른쪽 결합 연산자들이 옆에 쓰여져 있을 경우 오른쪽으로 결합니다. 아무 쪽도 아닌 결합 연산자들은 같은 우선 순위를 가진 다른 연산자들 옆에 쓰여질 수 없습니다.
결합 방법에 대한 조건은 특별히 이야기되지 않는다면 아무 쪽도 아닌 게 기본입니다. 우선순위의 경우 특별히 이야기되지 않는다면 100이 기본입니다.
다음의 예제는 '+-'라고 불리는 새로운 사용자 정의 중위 연산자를 정의합니다. 이때 이 연산자는 왼쪽 결합이며 140의 우선순위를 가집니다.
operator infix +- { associativity left precedence 140 }
func +- (left: Vector2D, right: Vector2D) -> Vector2D {
return Vector2D(x: left.x + right.x, y: left.y - right.y)
}
let firstVector = Vector2D(x: 1.0, y: 2.0)
let secondVector = Vector2D(x: 3.0, y: 4.0)
let plusMinusVector = firstVector +- secondVector
// plusMinusVector는 (4.0, -2.0)의 값들을 가지는 Vector2D 인스턴스입니다.
이 연산자는 두 벡터의 x값들을 더하고 첫 번째 것의 y로부터 두 번째 벡터의 y값을 뺍니다. 그것은 본질적으로 덧셈 연산자이기 때문에, '+'나 '-'와 같은 기본 덧셈 중위 연산자들과 같은 결합순위와 우선순위(왼쪽, 그리고 140)가 주어집니다. 기본적인 Swift 연산자 우선순위 및 결합순위 설정에 대한 완벽한 목록에 대해서는 "Expressions"장을 참조하세요.
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
Translator : 이름 (메일주소)
준비중
번역에 힘 써주신 고마우신 분들
(알파벳 순서)