Swift

Kyu hyunSung·2025년 4월 15일

IOS

목록 보기
9/9
post-thumbnail

Swift 언어 개발문서

1. Swift 핵심 철학

  1. 오픈소스 (Open Source)
  • 커뮤니티 기반 개발
  • 누구나 기여 가능
  1. 안전성 (Safe)
  • 엄격한 문법으로 오류 예방
  • 옵셔널, guard 구문 제공
  • 강력한 타입 체계

3.신속성 (Fast)

  • C 언어 수준의 성능
  • 최적화된 실행 속도

4.표현성 (Expressive)

  • 간결하고 읽기 쉬운 문법
  • 세미콜론 등 불필요 요소 제거
  1. 사용 환경

2. 문법

2-1. 변수, 컨테이너 변수

  1. 변수 (Variable)
  • 변수는 var 키워드로 선언
var name = "홍길동"      // 문자열 변수
var age = 25           // 정수형 변수
var height = 175.5     // 실수형 변수
  • 값을 변경할 수 있음.
var myVariable = 42 // 값 변경 O
Myvariable += 42 

상수 (Constant)

  • 상수는 let 키워드로 선언
  • 한 번 값이 할당되면 변경할 수 없음.
let pi = 3.14159       // 상수
let birthYear = 1995   // 상수
  • 한 번 값이 할당되면 변경할 수 없음.
let yourVariable = 42 // 42 고정
yourVariable += 42 // X

타입 명시

  • 타입을 명시적으로 지정할 수도 있고, 암시적으로 지정 수도 있음
  • 지정하지 않으면(암시적 지정) rvalue의 타입으로 추론하여 지정
var name: String = "홍길동"    // 문자열 타입 명시
var age: Int = 25             // 정수형 타입 명시
var height: Double = 175.5    // 실수형 타입 명시
var isStudent: Bool = true    // 불리언 타입 명시
let explicitInt: Int = 70 
// 변수 explicitInt의 타입을 명시적으로 Int로 지정 
let inplicitInteger = 70 
// 변수 inplicitInteger의 타입은 지정되지 않았으나 rvalue인 70을 보고 Int로 지정 
let implicitDouble = 70.5 
let explicitDouble: Double = 70 
// 자동형 변환이 되는지… 확인요망 

let Forcefloat : Float = 4
// 실습: 명시적으로 Float 타입인 상수를 만들고 4라는 값을 할당해보자
  1. 기본 데이터 타입

Int: 정수 (1, 2, 100, -5 등)
Double: 64비트 부동소수점 (3.14, 0.25 등)
Float: 32비트 부동소수점 (3.14f, 0.25f 등)
Bool: 참/거짓 (true, false)
String: 문자열 ("안녕하세요")
Character: 단일 문자 ("A", "가")

형변환

  • 기본적으로 좌.우 동일한 타입이어야 함
  • 생성자의 호출로 변환
  • Int, Unit, Float, Double, String, Bool
let label = "The width is " 
let width = 94 
let widthLabel = label + String(width) 
// String는 String의 생성자를 호출 
// label , width 둘다 String 이여야함

let height = Int(10) 
let sheight = Int(10.5) 
let dheight = Int(10.5) 
// 실습: 마지막 줄에 명시한 String 타입 변환 부분을 제거해보자. 어떤 에러가 발생하는가?
ERROR!
/tmp/h5CIZIHn1S/main.swift:9:24: error: binary operator '+' cannot be applied to operands of type 'String' and 'Int'
let widthLabel = label + width
                 ~~~~~ ^ ~~~~~
/tmp/h5CIZIHn1S/main.swift:9:24: note: overloads for '+' exist with these partially matching parameter lists: (Int, Int), (String, String)
let widthLabel = label + width

Swift는 강한 타입 검사(strict type checking)를 하는 언어. 이는 다른 언어들과 달리 서로 다른 타입을 자동으로 변환하지 않는다는 의미

  1. 문자열 처리

문자열 처리

let firstName = "길동"
let lastName = "홍"
let fullName = lastName + firstName  // "홍길동"

문자열 보간법 (String Interpolation)

  • 문자열로 변환: ()
  • 문자열안에 변수 값은 문자열로 변환하여 적용
let name = "홍길동"
let age = 25
// \()가 문자열 안에 있다
let introduction = "\(name)의 나이는 \(age)세입니다."
// "홍길동의 나이는 25세입니다."

// 실습: \() 를 이용해 문자열 안에 실수형 계산을 포함하도록 해보고, 인사말 안에 누군가의 이름을 넣어보자.
// 1. 문자열 안에 실수형 계산 포함하기
let width = 2.5
let height = 3.0
let area = width * height

// 문자열 보간법을 사용해 계산 결과 포함하기
let message = "직사각형의 면적은 \(width) × \(height) = \(area) 제곱미터입니다."
print(message)

// 2. 인사말 안에 누군가의 이름 넣기
let name = "철수"
let greeting = "안녕하세요, \(name)님! 오늘 좋은 하루 되세요."
print(greeting)

// 직접 계산식을 문자열 안에 넣을 수도 있습니다
let pricePerItem = 1000.0
let quantity = 3
let total = "총 가격: \(pricePerItem * Double(quantity))원"
print(total)
직사각형의 면적은 2.5 × 3.0 = 7.5 제곱미터입니다.
안녕하세요, 철수님! 오늘 좋은 하루 되세요.
총 가격: 3000.0원

여러 줄 문자열

let poem = """
봄바람이
살랑살랑
불어옵니다.
"""
  1. 컬렉션 타입

배열(array): [] 대괄호 를 이용하여 만든다

var fruits = ["사과", "바나나", "오렌지"]
fruits.append("딸기")         // 배열에 요소 추가
let firstFruit = fruits[0]   // 배열 인덱싱 (사과)
// 배열 선언 
var list1: [Int] = [] 
// Int를 저장하는 빈 배열, 명시적 
var list2 =[Int]() 
// Int를 저장하는 빈 배열, 암시적

var shoppingList = ["catfish", "water", "tulips", "blue paint"] 
// 배열초기화, 암시적 shoppingList[1] = "bottle of water" 
// 배열 요소 액세스 
let first = shoppingList.first // shoppingList[0]

딕셔너리 (Dictionary) : <key, value>로 값을 저장하는 구도

var scores = ["수학": 90, "영어": 85, "국어": 95]
scores["과학"] = 88          // 새 키-값 쌍 추가
let mathScore = scores["수학"]  // 값 접근 (90)
// 딕셔너리 선언 
var dict1: [String: Int] = [:] 
// <문자열, 값>으로 저장하는 딕셔너리, 명시적 
var dict2 = Dictionary<String, Int>() 
// <문자열, 값>으로 저장하는 딕셔너리, 암시적 

var occupations = [ "Malcolm":"Captian", "Kaylee":"Mechanic", ] 
//딕셔너리 초기화 
occupations["Jayne"] = "Public Relationslet myjob = occupations[“jmlee”] 
// 에러, “jmlee”가 저장되어 있지 않기 때문

세트 (Set)

  • 세트(Set)는 중복 없이 순서를 유지하지 않는 컬렉션으로, 빠른 검색과 합집합/교집합 같은 집합 연산에 효율적인 Swift의 데이터 구조입니다.
var uniqueNumbers: Set<Int> = [1, 2, 3, 4, 5]
uniqueNumbers.insert(6)      // 요소 추가
uniqueNumbers.contains(3)    // 요소 확인 (true)

2-2. 흐름제어

  1. 조건문
    if-else 문
  • f문 안의 조건은 꼭 불리언(Boolean) 표현이어야 함. if score {...}라고 표현하면 0과의 비교를 암시하지 않기 때문에 에러가 발생 if(…)에서 괄호를 없애고 if … 해도 된다.
let score = 75

// 1. 올바른 방법: 불리언 표현식 사용
if score > 50 {
    print("높은 점수입니다")
}

// 2. 잘못된 방법: 숫자만 사용하면 에러 발생
// if score {  // 컴파일 에러: 'Int' 타입은 불리언 컨텍스트에서 사용할 수 없음
//     print("점수가 0이 아닙니다")
// }

// 3. 반드시 불리언 표현식을 명시적으로 사용해야 함
if score != 0 {
    print("점수가 0이 아닙니다")
}

// 4. 괄호 사용은 선택사항
if (score > 60) {
    print("합격입니다 (괄호 사용)")
}

if score > 60 {
    print("합격입니다 (괄호 없음)")
}

// 5. 다른 언어와의 비교 - JavaScript에서는 아래 코드가 유효함
// JavaScript 예시: if(score) { console.log("점수가 0이 아닙니다") }
// Swift에서는 반드시 명시적 비교가 필요합니다
var teamScore = 0
let score = 30

if score > 50 {
    teamScore = 3
} else {
    teamScore = 1
}

print(teamScore)

switch 문

  • switch문에는 정수 타입 값이나 동등 비교 연산 뿐만 아니라 어떤 종류의 데이터 든 사용할 수 있고 다양
  • switch문은 case문의 조건과 일치할 때 case문 아래에 속한 코드를 실행하고 switch문을 빠져나옴.
  • 이후에 나오는 case문은 자동적으로 실행하지 않기 때문에 코드 끝에 break 키워드를 명시하지 않아도 됨
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 부분을 제거해 보자. 어떤 에러가 발생하는가?
error: switch must be exhaustive
switch vegetable {
^
note: do you want to add a default clause?
switch vegetable {
^
---
Swift의 switch문은 모든 가능한 경우를 처리해야 하는 '완전성(exhaustive)' 규칙이 있습니다.
default 케이스를 제거하면 "switch must be exhaustive"(switch문이 완전해야 함) 에러가 발생합니다.
String 타입은 가능한 값이 무한히 많아 명시적 case만으로는 모든 경우를 처리할 수 없으므로 default가 필요합니다.
// 문자열 상수 vegetable을 선언하고 "red pepper"라는 값을 할당합니다.
// Swift의 switch문은 문자열, 정수, 부동소수점, 불리언 등 다양한 데이터 타입을 처리할 수 있습니다.
let vegetable = "red pepper"
print("Selected vegetable: \(vegetable)")

// vegetable 변수를 기준으로 switch문을 시작합니다.
// 변수를 switch 밖에서 선언하여 결과값을 저장할 수 있게 합니다.
var vegetableComment = ""

switch vegetable {
// 첫 번째 case: vegetable이 "celery"와 정확히 일치하는지 확인합니다.
// 일치하면 아래 코드를 실행하고 자동으로 switch문을 빠져나갑니다(break 필요 없음).
case "celery":
    vegetableComment = "Add some raisins and make ants on a log."
    print("Celery case matched!")
    
// 두 번째 case: vegetable이 "cucumber" 또는 "watercress"와 일치하는지 확인합니다.
// 쉼표로 구분하여 여러 조건을 하나의 case에서 처리할 수 있습니다.
// 일치하면 아래 코드를 실행하고 자동으로 switch문을 빠져나갑니다.
case "cucumber", "watercress":
    vegetableComment = "That would make a good tea sandwich."
    print("Cucumber or watercress case matched!")
    
// 세 번째 case: 패턴 매칭과 조건문(where)을 함께 사용합니다.
// let x는 vegetable 값을 임시 변수 x에 바인딩합니다.
// where 절은 추가 조건을 지정하며, 이 경우 x가 "pepper"로 끝나는지 확인합니다.
// "red pepper"는 "pepper"로 끝나므로 이 case와 일치합니다.
// \(x)는 문자열 보간법으로 실제 vegetable 값을 메시지에 포함합니다.
case let x where x.hasSuffix("pepper"):
    vegetableComment = "Is it a spicy \(x)?"
    print("Pepper suffix case matched! Found: \(x)")
    
// default case는 앞선 모든 case가 일치하지 않을 때 실행됩니다.
// Swift의 switch문은 완전성(exhaustive)이 요구되므로, 
// 모든 가능한 값을 처리하기 위해 default case가 필요합니다.
default:
    vegetableComment = "Everything tastes good in soup."
    print("Default case matched!")
}

// switch문 실행 후 최종 결과 출력
print("Final comment: \(vegetableComment)")
  1. 반복문
    for in range ( range객체 )
    • let range = 0..<10
    • let range = 0…10
// 첫 번째 for 루프 예제: 반열린 범위(half-open range) 사용
var firstForLoop = 0
var range = 0..<5     // 0, 1, 2, 3, 4를 포함하는 범위 (5는 포함하지 않음)

for i in range {
    firstForLoop += i  // 각 반복마다 i 값을 더함
}

print("firstForLoop=", firstForLoop)  // 결과: 0+1+2+3+4 = 10

// 두 번째 for 루프 예제: 닫힌 범위(closed range) 사용
var secondForLoop = 0

for _ in 1...5 {      // 1, 2, 3, 4, 5를 포함하는 범위
    secondForLoop += 1  // 반복 값을 사용하지 않고(_) 단순히 5번 반복
// _ 를 사용하면 필요 없는 변수를 생성하지 않는다.
}

print("secondForLoop=", secondForLoop)  // 결과: 1+1+1+1+1 = 5

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 {
    // (kind, numbers)대신에 (_, numbers) 가 더 좋다
    // kind 변수를 사용하지 않는다면 와일드카드 패턴(_)을 사용하는 것이 좋음
    
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}

print("The largest number is \(largest)")  // 결과: 25

while

// while 루프 예제
var n = 2
while n < 100 {
    n = n * 2
}
print("최종 n 값:", n)  // 결과: 128

// repeat-while 루프 예제 (다른 언어의 do-while과 동일)
var m = 2
repeat {
    m = m * 2
} while m < 100
print("최종 m 값:", m)  // 결과: 128

//do{ }while 대신에 repeat {} while를 사용한다.
// Swift에서는 do-while 대신 repeat-while 문법을 사용함

2-3. 함수

  1. 함수
  • func를 사용해 함수를 선언
  • 함수를 호출할 때 함수의 이름과 괄호안에 인자들을 넣을 수 있음
  • 매개변수의 이름과 분리해서 '->'를 사용해 타입 이름을 표기하면 함수 반환 값 의 타입을 지정
// Swift에서 func 키워드를 사용하여 함수를 선언합니다
func greet(name: String, day: String) -> String {
    // -> String은 함수의 반환 타입이 String임을 나타냅니다
    // 반환값이 없는 경우 -> 생략 또는 -> Void로 표기합니다
    return "Hello \(name), today is \(day)."
    // \(변수명)을 사용하여 문자열 내에 변수 값을 삽입할 수 있습니다
}

// 함수 호출 시 매개변수 이름(name:, day:)을 명시해야 합니다
greet(name: "Bob", day: "Tuesday")

// Swift에서는 매개변수 순서가 중요합니다
// 파이썬과 달리 매개변수 순서를 바꾸면 오류가 발생합니다
greet(day: "Tuesday", name: "Bob")
  1. 전달 매개변수
  • 함수의 매개변수에서 호출시 사용하는 매개변수
// 전달 매개변수(argument label)와 매개변수 이름(parameter name) 모두 지정한 함수
func greet(from name: String, today day: String) -> String {
    // from: 전달 매개변수, name: 함수 내부에서 사용하는 매개변수 이름
    // today: 전달 매개변수, day: 함수 내부에서 사용하는 매개변수 이름
    return "Hello \(name), today is \(day)."
}

// 함수 호출 시 전달 매개변수 이름을 사용해야 함
print(greet(from: "Bob", today: "Tuesday")) // 정상 작동

// 함수 내부 매개변수 이름으로 호출하면 에러 발생
// print(greet(name: "Bob", day: "Tuesday")) // 에러
  • 전달 매개변수의 생략
// 밑줄(_)을 사용하여 두 번째 전달 매개변수를 생략한 함수
func greet(from name: String, _ day: String) -> String {
    // from: 전달 매개변수, name: 함수 내부에서 사용하는 매개변수 이름
    // _: 전달 매개변수 생략, day: 함수 내부에서 사용하는 매개변수 이름
    return "Hello \(name), today is \(day)."
}

// 두 번째 매개변수는 전달 매개변수 없이 값만 전달
print(greet(from: "Bob", "Tuesday")) // 정상 작동

// 함수 내부 매개변수 이름으로 호출하면 에러 발생
// print(greet(name: "Bob", day: "Tuesday")) // 에러
  1. 배열
  • 함수의 매개변수에서 호출시 사용하는 매개변수
// 가변 인자(Variadic Parameters)를 사용한 함수
// Int... 문법은 여러 개의 Int 값을 받을 수 있게 함
func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}

// 가변 인자 함수 호출 예시
print(sumOf()) // 0 (인자가 없으면 빈 배열로 처리)
print(sumOf(numbers: 42, 597, 12)) // 651 (여러 값을 콤마로 구분하여 전달)
  • 다음은 위와 동일
// 배열을 매개변수로 받는 함수
func sumOf1(numbers: [Int]) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}

// 배열 매개변수 함수 호출 예시
print(sumOf1(numbers: [42, 597, 12])) // 651 (배열을 직접 전달)

10.중첩 함수

  • 함수는 중첩해서 사용할 수도 있음
  • 중첩(Nested) 함수는 감싸고 있는 함수에서 선언된 변수에 접근할 수 있음
// 중첩 함수(Nested Function) 예시
func returnFifteen() -> Int {
    // 외부 함수에서 변수 선언
    var y = 10
    
    // 내부에 함수 정의 (중첩 함수)
    func add() {
        // 중첩 함수는 외부 함수의 변수에 접근 가능
        y += 5
    }
    
    // 중첩 함수 호출
    add()
    
    // 변경된 변수 값 반환
    return y
}

// 함수 호출 결과: 15
returnFifteen()
  1. 함수도 최상위 타입(first class type)
  • 최상위 타입이란
    • 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체(상수, 변수 등)
    • 보통 함수에 매개변수로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 최상위 타입 객체
  • 어떤 함수가 다른 함수를 반환 값 형태로 반환할 수 있다는 것을 의미
// 함수를 반환하는 함수 예시
// 반환 타입 (Int) -> Int는 "Int를 받아서 Int를 반환하는 함수"를 의미함
func makeIncrementer() -> (Int) -> Int {
    // 중첩 함수 정의
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    
    // 함수를 반환값으로 돌려줌
    return addOne
}

// 반환된 함수를 변수에 할당
var increment = makeIncrementer()

// 할당된 함수 사용 - 결과: 8
increment(7)
  • 함수도 매개변수로 받을 수도 있음
// 함수를 매개변수로 받는 함수 정의
// 두 번째 매개변수 condition은 Int를 받아 Bool을 반환하는 함수 타입
func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    // 배열의 각 요소에 대해
    for item in list {
        // 전달받은 condition 함수에 현재 요소를 전달하여 평가
        if condition(item) {
            return true // 조건을 만족하는 요소가 하나라도 있으면 true 반환
        }
    }
    return false // 조건을 만족하는 요소가 없으면 false 반환
}

// 판별 함수 정의 - 숫자가 10보다 작은지 확인
func lessThanTen(number: Int) -> Bool {
    return number < 10
}

// 테스트할 배열
var numbers = [20, 19, 7, 12]

// 함수를 매개변수로 전달하여 호출
// 결과: true (배열에 7이 있어 10보다 작은 요소가 존재함)
hasAnyMatches(list: numbers, condition: lessThanTen)

12.클로저

  • 이름없는 함수: {}
  • 실제로 함수는 클로저(Closure)의 특별한 예
  • 중괄호로 묶어서 이름을 지정하지 않고도 함수로 사용
  • n 키워드를 사용해 인자와 반환값 타입을 분리해 사용
// 클로저 사용 예시
let numbers = [10, 21, 30]

// 클로저 표현식을 map 함수의 인자로 직접 전달
let y = numbers.map({ (number: Int) -> Int in
    // 'in' 키워드로 클로저의 매개변수와 본문을 구분
    return 3 * number
})

// 결과: [30, 60, 90]
print(y)
// 함수를 사용한 동일한 결과 구현
let numbers2 = [10, 21, 30]

// 이름 있는 함수 정의
func threeTimes(number: Int) -> Int {
    return 3 * number
}

// 함수 이름을 map의 인자로 전달
let y2 = numbers2.map(threeTimes)

// 결과: [30, 60, 90]
print(y2)
  • 배열의 map 함수

  • 클로저의 간결화

// 1. 기본 형태의 클로저
let numbers1 = [10, 20, 30]
var y1 = numbers1.map({ (number: Int) -> Int in
    let result = 3 * number
    return result
})

// 2. 매개변수 타입 생략 (컴파일러가 타입 추론)
let numbers2 = [10, 20, 30]
var y2 = numbers2.map({ (number) -> Int in
    let result = 3 * number
    return result
})

// 3. 반환 타입 생략 및 단일 표현식으로 축약
// 단일 표현식일 경우 return 키워드 생략 가능
let numbers3 = [10, 20, 30]
var y3 = numbers3.map({ (number) in 3 * number })
  • 매개변수 번호
    • 매개변수의 이름 대신에 번호로 참조하는 것은 클로저를 짧게 만드는데 특히 유용함
    • 이때 클로저는 함수의 바로 뒤에 중괄호를 이용해 인자로 전달
      • $0, $1, $2 …
// 1. 매개변수 이름을 사용한 클로저
let numbers1 = [10, 20, 30]
var y1 = numbers1.map({ number in 3 * number })

// 2. 매개변수 번호를 사용한 더 간결한 클로저
// $0은 첫 번째 매개변수를 의미
let numbers2 = [10, 20, 30]
var y2 = numbers2.map({ 3 * $0 })

2-4 클래스, 구조체

  1. 클래스
  • 클래스를 만들기 위해서는 클래스 이름과 함께 class 키워드를 사용
  • 속성을 선언하기 위해서는 상수나 변수를 선언하는 것과 똑같은 방식
  • 메서드와 함수도 선언
// 클래스 정의
class Shape {
   // 속성(프로퍼티) 선언 - 인스턴스 변수
   var numberOfSides = 0
   
   // 메서드 정의 - 인스턴스 함수
   func simpleDescription() -> String {
       // 문자열 보간법(String Interpolation)을 사용하여 변수값을 문자열에 포함
       return "A shape with \(numberOfSides) sides."
   }
}

// 인스턴스 생성 - Swift에서는 new 키워드 없이 생성자 호출
var shape = Shape()

// 점(.) 연산자로 속성에 접근하여 값 변경
shape.numberOfSides = 7

// 점(.) 연산자로 메서드 호출
var shapeDescription = shape.simpleDescription()

// Swift 클래스의 특징:
// 1. new 키워드 없이 인스턴스 생성
// 2. 속성(프로퍼티)과 메서드를 가질 수 있음
// 3. 기본값으로 속성 초기화 가능
// 4. 점(.) 연산자로 속성과 메서드에 접근
// 5. 문자열 보간법 사용 가능 - \(변수명)
// 6. 참조 타입으로 작동 (값 타입인 구조체와 대비됨)
// 7. 상속, 다형성, 캡슐화 등 객체지향 프로그래밍 지원
  • 초기화자(생성자)
    - init 키워드 사용
    // NamedShape: 도형의 기본 속성을 정의하는 클래스
class NamedShape {
    // 도형의 변의 수를 저장하는 속성
    // - Int 타입의 non-optional 변수
    // - 초기값 0으로 선언되어 있어 별도 초기화 필요 없음
    var numberOfSides: Int = 0
    
    // 도형의 이름을 저장하는 속성
    // - String 타입의 non-optional 변수
    // - 초기값이 없으므로 반드시 초기화 필요
    var name: String
    
    // 생성자(initializer)
    // - name 파라미터를 받아 객체 초기화
    init(name: String) {
        // self.name에 파라미터 값 할당하여 초기화
        // name 속성은 non-optional이므로 반드시 이 시점에서 초기화해야 함
        self.name = name
    }
    
    // 도형에 대한 간단한 설명을 반환하는 메서드
    // - 반환 타입: String
    func simpleDescription() -> String {
        // 문자열 보간법(String interpolation)을 사용하여 도형 정보 반환
        return "A shape with \(numberOfSides) sides."
    }
}
  • 파괴자
    - deinit 키워드 사용
class NamedShape {
    var numberOfSides: Int = 0
    var name: String
    
    // 초기화 메서드(생성자)
    init(name: String) {
        print("\(name) 객체가 초기화됩니다.")
        self.name = name
    }
    
    // 소멸자 - 객체가 메모리에서 해제될 때 호출됨
    deinit {
        print("\(name) 객체가 메모리에서 해제됩니다.")
    }
    
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

// deinit 동작 확인을 위한 코드
func createAndDestroyShape() {
    // 함수 내부에서 NamedShape 인스턴스 생성
    // 함수가 종료되면 이 인스턴스는 스코프를 벗어나 메모리에서 해제됨
    let square = NamedShape(name: "사각형")
    square.numberOfSides = 4
    print(square.simpleDescription())
    // 함수가 여기서 종료되면 square 객체의 deinit이 호출됨
}

// 함수 호출
createAndDestroyShape()

// 옵셔널 변수와 함께 사용하는 예제
var circle: NamedShape? = NamedShape(name: "원")
circle?.numberOfSides = 0 // 원은 곡선이므로 sides는 0으로 설정
print(circle?.simpleDescription() ?? "")

// nil을 할당하여 메모리에서 해제
circle = nil // 이 시점에 deinit 호출됨
  • override
    - 하위 클래스에서 상위 클래스에서 구현된 메서드를 오버라이드(override) 하려면 override 키워드를 이용해 표시해줘야 함
// Square 클래스: NamedShape를 상속받는 사각형 클래스
class Square: NamedShape {
    // 사각형의 한 변의 길이를 저장하는 속성
    var sideLength: Double
    
    // 초기화 메서드 (생성자)
    // - sideLength와 name을 매개변수로 받음
    init(sideLength: Double, name: String) {
        // 1. 자신의 속성부터 초기화
        self.sideLength = sideLength
        
        // 2. 부모 클래스의 초기화 메서드 호출
        super.init(name: name)
        
        // 3. 부모로부터 상속받은 속성 설정
        numberOfSides = 4
    }
    
    // 사각형의 넓이를 계산하는 메서드
    func area() -> Double {
        return sideLength * sideLength
    }
    
    // 부모 클래스의 메서드를 재정의(override)
    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}

// Square 인스턴스 생성 및 사용
let test = Square(sideLength: 5.2, name: "my test square")
test.area()           // 결과: 27.04
test.simpleDescription() // 결과: "A square with sides of length 5.2."
// 실습: NamedShape클래스의 또 다른 하위 클래스인 Circle을 만들어보자. 
// 이 클래스는 이니셜 라이저를 통해 radius와 name을 인자로 받는다. Circle 클래스 안에 area, simpleDescription 함수를 구현해보자
// 요구사항에 따른 Circle 클래스 구현
class Circle: NamedShape {
    // 원의 반지름을 저장하는 속성
    var radius: Double
    
    // 초기화 메서드 (생성자)
    init(radius: Double, name: String) {
        // 1. 자신의 속성부터 초기화
        self.radius = radius
        
        // 2. 부모 클래스의 초기화 메서드 호출
        super.init(name: name)
        
        // 3. 원은 곡선이므로 변의 수는 0으로 설정
        numberOfSides = 0
    }
    
    // 원의 넓이를 계산하는 메서드
    func area() -> Double {
        return Double.pi * radius * radius
    }
    
    // 부모 클래스의 메서드를 재정의
    override func simpleDescription() -> String {
        return "A circle with radius of \(radius)."
    }
}

// Circle 클래스 사용 예시
let myCircle = Circle(radius: 2.5, name: "my circle")
myCircle.area()           // 결과: 약 19.63 (π × 2.5²)
myCircle.simpleDescription() // 결과: "A circle with radius of 2.5."
  • getter/setter
    - 저장되어 있는 간단한 속성 외에도 속성은 getter와 setter를 가질 수 있음
// EquilateralTriangle: 정삼각형 클래스 (NamedShape 상속)
class EquilateralTriangle: NamedShape {
    // 저장 속성(Stored Property): 실제 값을 메모리에 저장하는 속성
    // - 클래스, 구조체의 인스턴스와 연관된 값을 저장
    var sideLength: Double = 0.0
    
    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }
    
    // 계산 속성(Computed Property): 값을 저장하지 않고 계산하는 속성
    // - getter와 setter를 통해 다른 속성에 기반한 값을 계산하거나 설정
    // - 메모리에 실제 값을 저장하지 않고, 접근할 때마다 계산됨
    // - 캡슐화(encapsulation)를 구현하는 방법 중 하나
    var perimeter: Double {
        // getter: 속성 값을 읽을 때 실행되는 코드 블록
        // - 속성에 접근할 때마다 호출되어 계산된 값 반환
        // - 다른 속성값을 가공하여 새로운 값 생성
        // - 값을 저장하지 않고 필요할 때마다 계산하므로 항상 최신 상태 유지
        get {
            // 정삼각형의 둘레 = 한 변의 길이 × 3
            return 3.0 * sideLength
        }
        
        // setter: 속성 값을 설정할 때 실행되는 코드 블록
        // - 속성에 새 값을 할당할 때마다 호출됨
        // - newValue: 새로 할당된 값을 참조하는 기본 파라미터명
        // - 다른 저장 속성의 값을 변경하는 로직 포함
        // - 속성 간의 일관성을 유지하는 데 유용
        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")

// getter 호출 - 계산 속성의 값을 읽을 때 자동으로 get 블록 실행
triangle.perimeter // 결과: 9.3 (3.1 × 3)

// setter 호출 - 계산 속성에 값을 할당할 때 자동으로 set 블록 실행
triangle.perimeter = 9.9 // sideLength가 3.3으로 변경됨 (9.9 ÷ 3)

// 변경된 저장 속성 확인
triangle.sideLength // 결과: 3.3
  1. 열거형
  • 클래스나 모든 알려진 타입들의 경우 열거형에 메서드를 포함할 수 있음
  • enum 키워드를 사용하면 열거형을 만들 수 있음
// 열거형(Enum) - Int 타입의 원시값(raw value)을 가지는 열거형 정의
enum Rank: Int {
    // case 키워드로 열거형의 각 케이스(멤버)를 정의
    // Ace에는 원시값 1을 명시적으로 할당
    case Ace = 1
    
    // 명시적으로 값을 할당하지 않으면 이전 케이스의 원시값에 1을 더한 값이 자동 할당됨
    // Two=2, Three=3, ... 순서대로 값이 증가
    case Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten
    
    // 계속해서 값이 증가: Jack=11, Queen=12, King=13
    case Jack, Queen, King
    
    // 열거형 내부에 메서드 정의 가능
    // self는 현재 열거형 값(케이스)을 참조
    func simpleDescription() -> String {
        // switch 문으로 현재 케이스에 따라 다른 문자열 반환
        switch self {
        case .Ace:  // 같은 타입 내에서는 .Ace와 같이 점 구문으로 간략하게 표현 가능
            return "ace"
        case .Jack:
            return "jack"
        case .Queen:
            return "queen"
        case .King:
            return "king"
        default:
            // 숫자 카드들은 원시값(rawValue)을 문자열로 변환하여 반환
            return String(self.rawValue)
        }
    }
}
// 실험: 두개의 Rank 값의 원본 값을 비교하는 함수를 만들어보자.
// Rank 열거형의 Ace 케이스 인스턴스 생성
let ace = Rank.Ace

// 원시값 접근: rawValue 속성을 통해 열거형 케이스의 원시값에 접근 가능
let aceRawValue = ace.rawValue  // 결과: 1

// 실험: 두 개의 Rank 값의 원시값을 비교하는 함수
func compareRanks(_ rank1: Rank, _ rank2: Rank) -> String {
    if rank1.rawValue > rank2.rawValue {
        return "\(rank1.simpleDescription()) is higher than \(rank2.simpleDescription())"
    } else if rank1.rawValue < rank2.rawValue {
        return "\(rank1.simpleDescription()) is lower than \(rank2.simpleDescription())"
    } else {
        return "\(rank1.simpleDescription()) is equal to \(rank2.simpleDescription())"
    }
}

// 함수 사용 예시
compareRanks(Rank.King, Rank.Queen)  // "king is higher than queen"
compareRanks(Rank.Ace, Rank.Ten)    // "ace is lower than 10"
  • 열거형에서 실제값(rawValue)
    - 제 값이 의미 있는 경우가 아니라면 굳이 첫번째 값을 제공할 필요가 없음
 // 원시값(raw value)이 없는 열거형 정의
// 원시값이 필요 없는 경우 타입을 명시하지 않아도 됨
enum Suit {
    // 각 케이스(멤버) 정의
    // 원시값이 없기 때문에 초기값을 지정하지 않음
    case Spades, Hearts, Diamonds, Clubs
    
    // 열거형 내부에 메서드 정의
    func simpleDescription() -> String {
        // switch 문으로 현재 케이스에 따라 다른 문자열 반환
        // 모든 케이스를 처리하므로 default는 필요 없음
        switch self {
        case .Spades:   // 같은 타입 내에서는 .케이스명 형태로 간략하게 참조 가능
            return "spades"
        case .Hearts:
            return "hearts"
        case .Diamonds:
            return "diamonds"
        case .Clubs:
            return "clubs"
        }
    }
    // 실험 Suit에 color 메서드를 추가해 보자. color 메서드는 스페이드와 클로버는 'black'을 반환하고 하 트와 다이아몬드는 'red'를 반환하도록 하면 됩니다
    // 실험: color 메서드 추가
    // 카드 무늬에 따라 색상을 반환하는 메서드
    func color() -> String {
        switch self {
        case .Spades, .Clubs:  // 여러 케이스를 함께 처리 가능
            return "black"
        case .Hearts, .Diamonds:
            return "red"
        }
    }
}

// Suit 열거형의 Hearts 케이스 인스턴스 생성
let hearts = Suit.Hearts

// 메서드 호출
let heartsDescription = hearts.simpleDescription()  // 결과: "hearts"

// 추가한 color 메서드 사용 예시
let heartsColor = hearts.color()  // 결과: "red"
let spades = Suit.Spades
let spadesColor = spades.color()  // 결과: "black"
  1. Struct
  • 구조체는 메서드나 이니셜라이저 같은 클래스와 비슷한 기능들을 지원
  • 클래스와 구조체의 가장 중요한 차이점 중 하나는
    - 구조체의 경우 코드 내에서 전 달될 때 값 복사 형태로 전달되지만
    - 클래스의 경우에는 참조 복사 형태로 전달된 다는 것
// 구조체(Struct) 정의
// 구조체는 값 타입(Value Type)으로, 전달될 때 복사됩니다
struct Card {
   // 구조체의 저장 속성(Stored Properties)
   var rank: Rank  // 이전에 정의한 Rank 열거형 타입 사용
   var suit: Suit  // 이전에 정의한 Suit 열거형 타입 사용
   
   // 구조체 메서드 - 카드 정보를 문자열로 반환
   func simpleDescription() -> String {
       // 각 속성의 메서드를 호출하여 결합된 문자열 생성
       return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
   }
   
   // 참고: 구조체는 기본 멤버와이즈 이니셜라이저(memberwise initializer)를 자동 제공합니다
   // init(rank: Rank, suit: Suit) { ... } 형태의 이니셜라이저가 자동 생성됨
}

// Card 구조체의 인스턴스 생성
// .Three, .Spades와 같이 열거형 타입 내에서 점 구문을 사용하여 간략하게 표현
let threeOfSpades = Card(rank: .Three, suit: .Spades)

// 메서드 호출
let threeOfSpadesDescription = threeOfSpades.simpleDescription()  // "The 3 of spades"

// 구조체와 클래스의 차이점:
// 1. 값 타입 vs 참조 타입
//    - 구조체(Struct): 값 타입(Value Type) - 변수나 상수에 할당되거나 함수에 전달될 때 값이 복사됨
//    - 클래스(Class): 참조 타입(Reference Type) - 객체의 메모리 주소(참조)가 복사되어 여러 변수가 동일 인스턴스 참조 가능
//
// 2. 상속
//    - 구조체: 상속을 지원하지 않음
//    - 클래스: 다른 클래스를 상속할 수 있음
//
// 3. 초기화
//    - 구조체: 모든 속성을 초기화하는 멤버와이즈 이니셜라이저가 자동 제공됨
//    - 클래스: 사용자가 직접 이니셜라이저를 구현해야 함
//
// 4. 소멸자(deinit)
//    - 구조체: 소멸자를 가질 수 없음
//    - 클래스: deinit 소멸자를 통해 메모리 해제 시 작업 수행 가능
//
// 5. 참조 카운팅
//    - 구조체: 참조 카운팅 없음
//    - 클래스: ARC(Automatic Reference Counting)를 통해 메모리 관리

// 구조체 사용 예시 - 값 복사
var card1 = threeOfSpades
var card2 = card1     // card1의 값이 복사되어 card2에 할당됨
card2.rank = .Ace     // card2의 속성만 변경됨
// 이제 card1은 여전히 3 of spades, card2는 ace of spades

// 클래스였다면:
// var card2 = card1  // 참조가 복사됨
// card2.rank = .Ace  // 동일한 객체의 속성이 변경되므로 card1.rank도 변경됨

16.Protocol

  • 자바에서 interface와 같다
  • 메소드외 프로퍼티도 가능하다.
    • 이 경우 읽기전용({get}), 읽고쓰기({get set})를 명시하여야 함
// 프로토콜(Protocol) 정의
// 프로토콜은 특정 기능이나 요구사항의 청사진(blueprint)을 정의
protocol ExampleProtocol {
   // 프로토콜 속성 요구사항
   // - 읽기 전용 속성 {get} 명시 (구현 클래스에서는 get만 구현하거나, get과 set 모두 구현 가능)
   // - 저장 속성이나 계산 속성으로 구현 가능
   var simpleDescription: String { get }
   
   // 프로토콜 메서드 요구사항
   // - 메서드 시그니처만 정의, 구현은 프로토콜을 채택한 타입에서 제공
   func adjust()
}

// 프로토콜을 채택(adopt)한 클래스
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

// 출력: "A very simple class. Now 100% adjusted."
print(aDescription)

// Swift 프로토콜과 Java 인터페이스 비교:
// 1. 공통점:
//    - 둘 다 타입이 구현해야 할 요구사항의 청사진 역할
//    - 다중 채택/구현 가능 (다중 상속과 유사한 기능)
//
// 2. 차이점:
//    - Swift 프로토콜: 속성 요구사항을 포함할 수 있음 (읽기 전용 {get} 또는 읽기/쓰기 {get set})
//    - Java 인터페이스: 전통적으로 메서드만 포함 (Java 8부터 기본 메서드 지원, Java 9부터 private 메서드 지원)
//
// 3. Swift 프로토콜의 특징:
//    - 클래스뿐만 아니라 구조체, 열거형에서도 채택 가능
//    - 프로토콜 확장(Protocol Extension)을 통해 기본 구현 제공 가능
//    - 프로토콜 합성(Protocol Composition)을 통해 여러 프로토콜의 요구사항 조합 가능
// Java 인터페이스 정의
public interface ExampleInterface {
    // 상수 선언 (public static final이 암시적으로 적용됨)
    String CONSTANT_VALUE = "This is a constant";
    
    // 추상 메서드 선언 (public abstract가 암시적으로 적용됨)
    // 구현부가 없고, 메서드 시그니처만 있음
    void adjust();
    
    // Java 8부터 추가된 기능: 기본 메서드(default method)
    // 인터페이스 내에서 구현을 제공할 수 있음
    default String getDescription() {
        return "Default description";
    }
    
    // Java 8부터 추가된 기능: 정적 메서드(static method)
    static void staticMethod() {
        System.out.println("This is a static method in interface");
    }
    
    // Java 9부터 추가된 기능: private 메서드
    // 인터페이스 내부에서만 사용 가능
    private void helperMethod() {
        System.out.println("Helper method");
    }
}

// 인터페이스를 구현(implements)하는 클래스
public class SimpleClass implements ExampleInterface {
    private String description = "A simple class";
    
    // 인터페이스의 추상 메서드 구현 (필수)
    @Override
    public void adjust() {
        description += " - adjusted";
    }
    
    // 기본 메서드 오버라이드 (선택 사항)
    @Override
    public String getDescription() {
        return description;
    }
    
    // 클래스 자체의 메서드 추가 가능
    public void additionalMethod() {
        System.out.println("Additional functionality");
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        // 인터페이스 타입으로 객체 참조 가능
        ExampleInterface obj = new SimpleClass();
        obj.adjust();
        System.out.println(obj.getDescription());
        
        // 정적 메서드 호출
        ExampleInterface.staticMethod();
        
        // 상수 접근
        System.out.println(ExampleInterface.CONSTANT_VALUE);
    }
}

17.Extenion

  • 기존의 타입들에 새로운 메서드나 속성들을 비교하기 위한 기능들을 추가
  • 기존 존재하는 기능을 재정의 할 수는 없다
  • 연산프로퍼티, 메소드, 이니셜라이저, 스브스크립트 등을 추가할 수 있음
// 프로토콜 정의 - 문자열 변환 기능을 위한 인터페이스
protocol ToString {
    // 객체를 문자열로 변환하는 메서드 요구사항
    func toString() -> String
}

// 익스텐션(Extension)을 사용하여 기존 타입(Date)에 프로토콜 구현 추가
extension Date: ToString {
    // 타입 속성(static property) - 모든 인스턴스가 공유하는 DateFormatter 인스턴스
    static let dateFormatter = DateFormatter()
    
    // ToString 프로토콜의 메서드 구현
    func toString() -> String {
        // 날짜 형식 설정 (yyyy-MM-dd)
        Date.dateFormatter.dateFormat = "yyyy-MM-dd"
        
        // 현재 날짜를 지정된 형식의 문자열로 변환하여 반환
        return Date.dateFormatter.string(from: self)
    }
}

// Date 인스턴스 생성
let now = Date()

// 확장을 통해 추가된 메서드 호출
print(now.toString()) // 예: "2025-04-14" (오늘 날짜)

// Swift의 익스텐션(Extension) 특징:
// 1. 기존 타입에 새로운 기능 추가
//    - 소스 코드 접근 없이도 타입을 확장 가능 (표준 라이브러리 타입 포함)
//    - 프로토콜 준수를 위한 구현을 별도로 추가 가능
//
// 2. 추가 가능한 기능:
//    - 계산 속성(Computed Properties) - 저장 속성은 추가 불가
//    - 메서드(Methods)
//    - 이니셜라이저(Initializers)
//    - 서브스크립트(Subscripts)
//    - 중첩 타입(Nested Types)
//    - 프로토콜 준수(Protocol Conformance)
//
// 3. 제한 사항:
//    - 기존 기능 재정의(override) 불가
//    - 저장 속성(Stored Properties) 추가 불가
//    - 타입의 상속 구조 변경 불가

2-5. 옵셔널

  1. Optional
  • Swift의 옵셔널(Optional)은 값이 있을 수도 있고 없을 수도 있는 상황을 안전하게 처리하기 위한 기능.
  • 어떤 변수는 임의의 순간에 값이 정해지지 않는 경우가 있다
  • 이런 변수를 Optional 변수라 한다

    The Basics

// Optional 변수 예제
// Optional: 값이 있을 수도 있고 없을 수도 있는 상태를 나타내는 타입

// Int("10")은 문자열을 정수로 변환하려는 시도를 나타냄
// 변환 성공 시 값 반환, 실패 시 nil 반환
var anumber = Int("10")    // anumber의 타입은 Int? (Optional Int)
                          // 값은 Optional(10) - 변환 성공

// "10.0"은 정수로 변환할 수 없으므로 nil 반환
var bnumber = Int("10.0") // bnumber의 타입은 Int? (Optional Int)
                          // 값은 nil - 변환 실패

// Optional 값 출력
print(anumber)  // 출력: Optional(10) - 값이 있음을 나타냄
print(bnumber)  // 출력: nil - 값이 없음을 나타냄

// 타입 불일치 에러 예제
// var cnumber: Int = Int("10")  // 에러! Int? 타입을 Int 타입에 할당 불가
                               // 해결책: 강제 언래핑(!) 또는 옵셔널 바인딩

// 올바른 해결책들:
var cnumber1: Int = Int("10")!  // 강제 언래핑 - 값이 nil이 아님이 확실할 때만 사용
// 또는
if let cnumber2 = Int("10") {   // 옵셔널 바인딩 - 안전한 방법
    // cnumber2는 여기서 Int 타입
}
// 또는
var cnumber3: Int = Int("10") ?? 0  // nil 병합 연산자 - nil일 경우 기본값 제공

// 주의할 점
// var dnumber: Int = Int("10.0")!  // 런타임 에러! nil을 강제 언래핑하면 크래시 발생
                                   // "10.0"은 Int로 변환 불가능하므로 Int("10.0")은 nil

// 안전한 방법:
if let dnumber = Int("10.0") {
    // 이 블록은 실행되지 않음 (Int("10.0")이 nil이므로)
    print("변환 성공: \(dnumber)")
} else {
    print("변환 실패") // 이 부분이 실행됨
}

// 또는 nil 병합 연산자 사용
var dnumber2: Int = Int("10.0") ?? 0  // 변환 실패 시 기본값 0 사용
  • 변수/상수 선언의 3가지
    – 일반: 일반값을 저장할수 있으나 절대 nil값은 저장할수 없다
    – Optional: 일반값과 nil값을 저장할 수 있다.
    – Implicit Unwrapping Optional: 일시적으로 nil을 가질수 있으나 read되기 전에 반드시 일반값이 저장되어야 한다. 아니면? → 죽는다
// 1. 일반 변수/상수: nil을 저장할 수 없음
var anumber: Int = 10      // 일반 변수 - nil 할당 불가, 반드시 값을 가져야 함

// 2. 옵셔널(Optional): 값 또는 nil을 저장할 수 있음
var bnumber: Int? = 20     // 옵셔널 변수 - 값이나 nil 저장 가능

// 3. 암시적 언래핑 옵셔널(Implicitly Unwrapped Optional): 
// 초기에는 nil일 수 있지만, 읽기 전에 값이 있어야 함
var cnumber: Int!          // 암시적 언래핑 옵셔널
var dnumber: Int!          // 암시적 언래핑 옵셔널

// 오류 사례 1: 암시적 언래핑 옵셔널이 초기화되지 않은 상태에서 접근
// anumber = cnumber      // 런타임 오류! - cnumber가 아직 nil

// cnumber 초기화 후 접근 - 안전함
cnumber = 20
anumber = cnumber          // 정상 작동 - cnumber가 값을 가지고 있음

// 오류 사례 2: 일반 변수에 옵셔널 할당 시도
// anumber = bnumber      // 컴파일 오류! - 타입 불일치
// 해결책: 강제 언래핑(!) 또는 옵셔널 바인딩(if let, guard let)
anumber = bnumber!         // 강제 언래핑 (bnumber가 nil이 아닐 경우만 안전)
// 또는
if let unwrapped = bnumber {
    anumber = unwrapped    // 옵셔널 바인딩 (안전한 방법)
}

// 암시적 언래핑 옵셔널에 옵셔널 값 할당 - 허용됨
dnumber = bnumber          // 정상 작동 - 둘 다 nil을 저장할 수 있음

// dnumber가 값이 있을 때 접근 - 안전함
anumber = dnumber          // 정상 작동 (dnumber가 값을 가지고 있을 때)

// dnumber를 nil로 설정
dnumber = nil

// nil인 암시적 언래핑 옵셔널에 접근 시도 - 위험함!
// anumber = dnumber      // 런타임 오류! - dnumber가 nil
  • 변수/상수 reading
    – 일반: 항상 reading할 수 있다
    – Implicit Unwrap Optional
    ( 코딩 타임에 이 변수에 nil이 아닌 값이 저장됨을 확신하여야 함 )
    – Optional: 여러가지 방법이 있다.
// 변수 선언의 세 가지 방식과 접근 방법

// 1. 일반 변수 - nil을 저장할 수 없음, 항상 안전하게 읽을 수 있음
var aint: Int = 10         // 일반 변수

// 2. 암시적 언래핑 옵셔널 - nil을 저장할 수 있지만 접근 시 자동 언래핑
var bint: Int! = 20        // 암시적 언래핑 옵셔널

// 3. 옵셔널 - nil을 저장할 수 있음, 접근 시 명시적 언래핑 필요
var cint: Int? = 30        // 옵셔널
var dint: Int?             // 초기값 미지정 = nil과 동일

// 출력 결과
print(aint)                // 출력: 10
print(bint)                // 출력: Optional(20) - 출력 시에는 옵셔널로 표시됨
print(cint)                // 출력: Optional(30)
print(dint)                // 출력: nil

// 값 할당

// 암시적 언래핑 옵셔널(Int!)은 일반 변수(Int)에 자동 언래핑되어 할당됨
aint = bint                // 문제 없음 - bint가 자동 언래핑됨

// 옵셔널(Int?)은 암시적 언래핑 옵셔널(Int!)에 할당 가능
bint = cint                // 문제 없음 - 둘 다 옵셔널이기 때문

// bint가 현재 cint의 값(Optional(30))을 가지므로 안전
aint = bint                // 문제 없음 - bint(값: 30)이 자동 언래핑됨

// nil을 암시적 언래핑 옵셔널에 할당
bint = dint                // 문제 없음 - bint는 이제 nil

// 여기서 문제 발생!
// aint = bint            // 런타임 오류! - nil인 값을 자동 언래핑하려고 시도
                          // 문법적으로는 오류가 없지만 실행 시 크래시 발생
  • Optional: If Statements and Forced Unwrapping
    • Forced Unwrapping: nil이 아닌 것이 너무나 명확한 경우 • “변수명!”로 데이터를 읽음
    • If statements: nil인지 아닌지 if문을 사용하여 체크
// Optional 처리 방법 예제

// 문자열을 정수로 변환 시도
let possibleNumber = "123"

// Int(String) 함수는 변환 가능한 경우 정수 값, 불가능한 경우 nil 반환
let convertedNumber = Int(possibleNumber)  // convertedNumber의 타입은 Int? (Optional Int)

// 방법 1: nil 체크 후 옵셔널 값 그대로 사용
if convertedNumber != nil {
    // convertedNumber는 여기서도 여전히 Optional 타입
    print("convertedNumber has an integer value of \(convertedNumber).")
}
// 출력: "convertedNumber has an integer value of Optional(123)."
// 옵셔널 값이 그대로 출력됨 (Optional(123))

// 방법 2: nil 체크 후 강제 언래핑(!) 사용
if convertedNumber != nil {
    // ! 연산자로 옵셔널에서 실제 값을 추출 (강제 언래핑)
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// 출력: "convertedNumber has an integer value of 123."
// 실제 정수 값만 출력됨 (123)
  1. Optional Binding
  • 이것은 앞의 If Statements and Forced Unwrapping를 합친 것과 같다
    – If문에서 nil이 아닌 경우 일반 변수를 변수를 만들어 준다
if let constantName = someOptional {
    // 이 블록 내에서만 constantName 변수가 유효함
    // someOptional이 nil이 아닐 경우에만 이 블록이 실행됨
}
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)  // convertedNumber의 데이터 타입은 Int?

if let actualNumber = Int(possibleNumber) {
    // 이 블록에서만 actualNumber가 유효함
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
    print("The string \"\(possibleNumber)\" has an integer value of \(Int(possibleNumber))")
} else {
    print("The string \"\(possibleNumber)\" couldn't be converted to an integer")
}
// 출력: "The string "123" has an integer value of 123"
  • Multple optional binding
    – If문안에서 comma로 분리하여 여러 개를 bindin할 수 있다
// 두 문자열을 정수로 변환하여 비교하는 함수
func comapre2numbers(first: String, second: String) -> String {
    // 다중 옵셔널 바인딩: 쉼표로 구분하여 여러 조건을 한 번에 체크
    if let firstNumber = Int(first), let secondNumber = Int(second) {
        // 모든 조건이 만족될 때만 이 블록이 실행됨
        // - first 문자열이 정수로 변환 가능
        // - second 문자열이 정수로 변환 가능
        // - firstNumber와 secondNumber는 이 블록 내에서만 유효한 지역 상수
        
        // 추가 조건 검사
        if firstNumber < secondNumber && secondNumber < 100 {
            return "\(firstNumber) < \(secondNumber) < 100"
        }
    }
    
    // 다음의 경우 "error" 반환:
    // - 문자열 변환 실패(Int 변환 불가)
    // - 조건 불만족(firstNumber >= secondNumber 또는 secondNumber >= 100)
    return "error"
}

// 함수 호출 및 결과 출력
print(comapre2numbers(first: "10", second: "42"))  // 출력: "10 < 42 < 100"
  • guard
    – 옵셔널 바이딩중 하나라도 nil이면 else로 빠져나옴
// 두 문자열을 정수로 변환하여 비교하는 함수
func comapre2numbers(first: String, second: String) -> String {
    // guard 구문을 사용한 다중 옵셔널 바인딩
    guard let firstNumber = Int(first), let secondNumber = Int(second) else {
        // 변환 실패 시(하나라도 nil이면) 여기서 함수를 종료
        return "error"
    }
    
    // guard 구문 이후부터는 firstNumber와 secondNumber가 non-optional 상수로 유효함
    // 함수의 나머지 부분에서 사용 가능
    
    // 추가 조건 검사
    if firstNumber < secondNumber && secondNumber < 100 {
        return "\(firstNumber) < \(secondNumber) < 100"
    }
    
    // 조건을 만족하지 않는 경우
    return "error"
}

// 함수 호출 및 결과 출력
print(comapre2numbers(first: "10", second: "42"))  // 출력: "10 < 42 < 100"
  1. Optional Chaining
  • 옵셔널 체이닝은 옵셔널 변수를 사용하여 하위 프로퍼티, 메소드, 스브스크립트 등을 액세스할 때
    옵셔널 변수가 nil인 경우 하위 프로퍼티, 메소드, 스브스크립 트 등의 액세스 없이 결과를 nil로 함
  • Optional Chaining as an Alternative to Forced Unwrapping
    – 액세스 하고자하는 프로퍼티, 메소드, 스브스크립트에 ?를 붙여서 표시
// 사람 클래스 - 거주지(residence)는 옵셔널 타입
class Person {
    var residence: Residence?  // 옵셔널 프로퍼티 - nil일 수 있음
}

// 거주지 클래스 
class Residence {
    var numberOfRooms = 1  // 방의 개수를 나타내는 일반 프로퍼티
}

// Person 인스턴스 생성
let john = Person()  // 기본적으로 residence는 nil로 초기화됨

// 강제 언래핑 시도 - 위험한 접근 방식
// let roomCount1 = john.residence!.numberOfRooms  
// 런타임 오류 발생! - residence가 nil이기 때문에 강제 언래핑 시 크래시 발생

// 옵셔널 체이닝 사용 - 안전한 접근 방식
let roomCount2 = john.residence?.numberOfRooms  
// residence가 nil이므로 ? 이후의 코드는 실행되지 않고 roomCount2는 nil이 됨
// roomCount2의 타입은 Int? (옵셔널 정수)

// nil 출력
print(roomCount2)  // nil
  • 체이닝의 다중 레벨 연결
    – 여러 레벨로 옵셔널 체이닝이 적용되는 경우
    – xxx?.yyy?.zzz? : 이 경우 xxx, yyy, zzz중 하나라도 nil이면 전체 결과가 nil이다
// 주소 클래스
class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    
    func buildingIdentifier() -> String? {
        if let buildingName = buildingName {
            return buildingName
        } else if let number = buildingNumber {
            return number
        } else {
            return nil
        }
    }
}

// 거주지 클래스
class Residence {
    var rooms: [String: Room] = [:]
    var numberOfRooms: Int = 0
    var address: Address?
    
    func roomWithName(_ name: String) -> Room? {
        return rooms[name]
    }
}

// 방 클래스
class Room {
    var name: String
    var furniture: [String]?
    
    init(name: String) {
        self.name = name
    }
}

// 사람 클래스
class Person {
    var residence: Residence?
}

// 사용 예시
let john = Person()
// 다중 레벨 옵셔널 체이닝
let roomName = john.residence?.roomWithName("Study")?.name
print(roomName) // nil - john.residence가 nil이므로

// residence 인스턴스 생성 및 할당
john.residence = Residence()

// 여전히 nil 반환 - rooms 딕셔너리에 "Study" 키가 없으므로
let studyRoomName = john.residence?.roomWithName("Study")?.name
print(studyRoomName) // nil

// 방 생성 및 추가
let study = Room(name: "Study Room")
john.residence?.rooms["Study"] = study

// 이제 방 이름에 접근 가능
let studyName = john.residence?.roomWithName("Study")?.name
print(studyName) // Optional("Study Room")

// 더 복잡한 체이닝: 가구 접근 시도
let chairName = john.residence?.roomWithName("Study")?.furniture?[0]
print(chairName) // nil - furniture가 nil이므로

// furniture 배열 설정
john.residence?.roomWithName("Study")?.furniture = ["Chair", "Desk", "Lamp"]

// 이제 가구에 접근 가능
let firstFurniture = john.residence?.roomWithName("Study")?.furniture?[0]
print(firstFurniture) // Optional("Chair")

// 주소 접근 시도
let streetName = john.residence?.address?.street
print(streetName) // nil - address가 nil이므로

// 주소 설정
john.residence?.address = Address()
john.residence?.address?.street = "123 Swift Street"

// 이제 주소에 접근 가능
let street = john.residence?.address?.street
print(street) // Optional("123 Swift Street")

// 메서드 호출을 포함한 체이닝
let buildingIdentifier = john.residence?.address?.buildingIdentifier()
print(buildingIdentifier) // nil (buildingName과 buildingNumber가 모두 nil이므로)

// buildingName 설정
john.residence?.address?.buildingName = "Swift Tower"

// 이제 buildingIdentifier 접근 가능
let buildingID = john.residence?.address?.buildingIdentifier()
print(buildingID) // Optional("Swift Tower")

2-6. 타입 캐스팅, 예외처리

  1. Type Casting
  • 타입 캐스팅
    – 형 변환: 클래스의 상속에서 형변환이 있음: as 키워드 사용
    – Down casting, Up casting가 있음
 // 기본 미디어 아이템 클래스
class MediaItem {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

// MediaItem을 상속한 영화 클래스
class Movie: MediaItem {
    var director: String
    
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

// MediaItem을 상속한 노래 클래스
class Song: MediaItem {
    var artist: String
    
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

// 다양한 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")
]
// library 배열의 타입은 [MediaItem]으로 추론됨
// 모든 요소가 MediaItem의 하위 클래스이므로 공통 상위 타입인 MediaItem으로 처리됨
  • 형 확인 (Checking Type): is 연산자
    – Object is Type: Object의 타입이 Type이면 true
 // 미디어 라이브러리에서 영화와 노래의 개수를 세는 코드
var movieCount = 0  // 영화 카운터 초기화
var songCount = 0   // 노래 카운터 초기화

// 라이브러리의 각 항목을 반복하며 타입 확인
for item in library {
    // is 연산자를 사용하여 타입 확인
    if item is Movie {
        // item이 Movie 타입이면 movieCount 증가
        movieCount += 1
    } else if item is Song {
        // item이 Song 타입이면 songCount 증가
        songCount += 1
    }
}

// 결과 출력
print("Media library contains \(movieCount) movies and \(songCount) songs")
// 출력: "Media library contains 2 movies and 3 songs"
  • 다운캐스팅 (Downcasting)
    – MeidaItem 타입의 객체를 Moive, Song객체로 캐스팅
    • as?, as!
    – 캐스팅이 안될 수도 있다.
    • as?: 캐스팅이 안되면 nil값을 주랴
    • as!: 캐스팅이 안되면 프로그램이 죽어도 좋다
// 미디어 라이브러리를 순회하며 각 항목의 구체적인 타입으로 다운캐스팅
for item in library {
    // 조건부 다운캐스팅(as?)을 사용하여 Movie 타입으로 변환 시도
    if let movie = item as? Movie {
        // 다운캐스팅 성공 시 Movie 타입의 특정 속성(director)에 접근
        print("Movie: \(movie.name), dir. \(movie.director)")
    } 
    // 조건부 다운캐스팅(as?)을 사용하여 Song 타입으로 변환 시도
    else if let song = item as? Song {
        // 다운캐스팅 성공 시 Song 타입의 특정 속성(artist)에 접근
        print("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

– as?, as!

// 강제 다운캐스팅(as!) - 확신이 있을 때 사용
let one = library[0] as! Movie  
// library[0]가 Movie 타입임이 확실하므로 성공적으로 다운캐스팅됨
// one은 Movie 타입 (non-optional)

// 조건부 다운캐스팅(as?) - 안전한 방식
let two = library[0] as? Movie  
// library[0]가 Movie 타입이므로 성공적으로 다운캐스팅됨
// two는 Movie? 타입 (optional)

// 조건부 다운캐스팅이 실패하는 경우
let four = library[1] as? Movie  
// library[1]은 Song 타입이므로 Movie로 다운캐스팅 실패
// four에는 nil이 할당됨 (타입은 Movie?)

// 강제 다운캐스팅이 실패하는 경우
// let five = library[1] as! Movie  
// library[1]은 Song 타입이므로 Movie로 다운캐스팅 실패
// 런타임 오류 발생! 앱이 충돌함
  1. Error Handling
  • 함수가 에러를 발생시킬 수 있음을 선언
func canThrowErrors() throws -> String
// throws 키워드는 이 함수가 에러를 던질 수 있음을 명시
// 에러를 던지는 함수를 호출할 때는 반드시 try를 사용해야 함
  • 에러를 던지지 않는 일반 함수
func cannotThrowErrors() -> String
// 일반 함수는 try 없이 호출 가능
  • 에러 처리 구문(try-catch)
do {
    try expression
    // 성공 시 실행
} catch pattern1 {
    // 특정 에러 처리
} catch {
    // 기타 에러 처리
}
  • try? (옵셔널 try): 에러 발생 시 nil 반환
let result = try? functionThatThrows()
// result는 옵셔널 타입, 에러 발생 시 nil
  • try! (강제 try): 에러가 발생하지 않을 것이 확실할 때만 사용
let result = try! functionThatThrows()
// 에러 발생 시 런타임 오류(앱 충돌)

ex 1 : 에러 정의 및 자판기 클래스 구현

// 자판기 오류 타입 정의
enum VendingMachineError: Error {
    case invalidSelection                      // 유효하지 않은 선택
    case insufficientFunds(coinsNeeded: Int)   // 잔액 부족(필요한 금액 포함)
    case outOfStock                            // 재고 부족
}

// 상품 구조체
struct Item {
    var price: Int
    var count: Int
}

// 자판기 클래스
class VendingMachine {
    // 재고 목록
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    
    // 상품 판매 메서드(에러를 던질 수 있음)
    func vend(itemNamed name: String) throws {
        // 상품이 존재하는지 확인
        guard let item = inventory[name] else {
            throw VendingMachineError.invalidSelection
        }
        
        // 재고가 있는지 확인
        guard item.count > 0 else {
            throw VendingMachineError.outOfStock
        }
        
        // 충분한 금액이 있는지 확인
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.insufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        // 모든 조건 충족 시 상품 제공
        coinsDeposited -= item.price
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        print("Dispensing \(name)")
    }
}

ex 2 : 에러 처리 방법 예제

// 자판기 인스턴스 생성 및 초기 금액 설정
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 32

// 방법 1: do-catch로 에러 처리
do {
    try vendingMachine.vend(itemNamed: "Chips")
} catch VendingMachineError.invalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.outOfStock {
    print("Out of Stock.")
} catch VendingMachineError.insufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch {
    print("Unexpected error: \(error)")
}
// 출력: Dispensing Chips, coinsDeposited: 22

// 방법 2: try? - 옵셔널 반환(에러 시 nil)
let x = try? vendingMachine.vend(itemNamed: "Chips")
print(x) // Dispensing Chips, coinsDeposited: 12, 출력: ()

// 방법 3: try! - 강제 실행(에러 없음 확신)
let y = try! vendingMachine.vend(itemNamed: "Candy Bar")
print(y) // Dispensing Candy Bar, coinsDeposited: 0, 출력: ()

// coinsDeposited가 0이므로 돈 부족
let z = try? vendingMachine.vend(itemNamed: "Pretzels")
print(z) // nil (돈 부족으로 에러 발생, 프로그램 계속 실행)

// 에러 발생 확신 시 try! 사용 위험
// 다음 코드는 런타임 오류 발생 (금액 부족 오류)
// let w = try! vendingMachine.vend(itemNamed: "Pretzels")
profile
디지털 치매 예방

0개의 댓글