swift 기본문법 - 사용자정의 연산자(Custom Operators)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


사용자정의 연산자(Custom Operators)

Swift는 사용자가 원하는 동작을 하는 연산자를 생성할 수 있다. 그렇기에 Swift에서 연산자는 부호로 표현하는 함수라고 할 수있다.

연산자의 종류

Swift의 연산자는 총 3 종류로 전위, 중위, 후위로 구성

1.전위 연산자: 연산자가 피연산자 앞에 위치하는 연산자

// 대표적인 예) 논리 부정 연산자
a = !a

2.중위 연산자: 연산자가 두 피연산자 사이에 위치하는 연산자

// 대표적인 예) 기본적인 연산자들
a + b
a += b
a - b

3.후위 연산자: 연산자가 피연산자 뒤에 위치하는 연산자

// 대표적인 예) 옵셔널 관련 연산자
a?
a!

연산자는 /, =, -, +, _, %, <, >, &, |, ^, ?, ~ 를 결합하여 만들 수 있다.

마침표(.)을 이용하여 사용자를 만들수 있지만 연산자 문자 중 맨 처음 문자가 마침표여야 하며 .+.는 사용 가능하지만 +.+와 같이 선언하면 +와 .+연산자 로 나누어 인식하게 된다. 물음표와 느낌표는 연산자에 포함 가능하지만 자체적으로 재정의는 불가능하다.

토큰 =. ->, //, /_, */전위 연산자 <, &, ? 중위 연산자 ? 후위 연산자 >, !, ? 역시 재정의가 불가능

전위 연산자

전위 연산자를 구현하기 위해 prefix라는 키워드를 사용

// 연산자를 구현하기 위해 미리 선언
prefix operator **

// 제곱을 수행하는 연산자를 정의
prefix func ** (num: Int) -> Int{
    return num * num
}
print(**100)
// 10000

위의 연산자를 재정의 하지 않고 기능만 추가하려면 연산자를 선언하지 말고 함수를 overload

//논리 부정 연산자를 String에서도 사용 가능하도록 기능을 추가
prefix func ! (str: String) -> Bool{
    return str.isEmpty()
}
// !을 통해 String의 값이 비었는지 확인 가능
print(!"")
// true

후위 연산자

후위 연산자를 구현하기 위해 postfix라는 키워드를 사용

postfix operator ++
// 전위와 같이 연산자를 구현하기 위해 미리 선언

// 값에 1을 더하는 연산자를 만든다
postfix func ++ (num: Int) -> Int{
    return num + 1
}
print(1++)
// 2

하나의 피연산자에 전위와 후위를 한 줄에 사용하게 되면 후위가 먼저 실행

print(**10++)
// (11) * (11)
// 121

중위 연산자

중위 연산자의 종류는 다양하다. 그렇기에 중위 연산자는 특별히 우선순위를 가지는데, 중위 연산자의 우선순위는 더 높은 우선순위, 더 낮은 우선순위, 결합방향, 할당방향을 명시하며 중위 연산자에 우선순위를 명시하지 않으면 우선순위가 가장 높은 DefalutPrecedence그룹이 할당된다.

infix operator <=
// 연산자를 미리 선언

// 서로의 문자의 수를 비교하는 연산자
func <= (str1: String, str2: String) -> Bool{
    return str1.size <= str2.size
}
print( "hello" <= "nice" )
// false

swift 기본문법 - 중첩타입(Nested types)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고


중첩타입(Nested types)

열거형(Enum)은 종종 특정 클래스나 구조체 기능을 지원하기 위해 만들어진다.

마찬가지로, 보다 복잡한 타입의 컨텍스트내에서 사용하기 위해, 유틸리티 클래스 및 구조체를 정의하는 것이 편리할 수 있습니다.
이를 달성하기 위해 Swift는 중첩된 타입(Nested Types)을 정의할 수 있는데 즉, 지원하는 타입의 정의내에서 클래스 및 구조체, 열거형을 중첩할 수 있다.

다른 타입에서 타입을 중첩하기 위해서는 타입의 괄호 밖에 정의해야 하며, 타입은 필요한 만큼의 여러 수준으로 중첩할 수 있다.

중첩 타입 사용

BlackjackCard라는 구조체는 블랙잭 게임에서 사용하는 게임 카드로 만들어 정의한다. BalckjackCard 구조체는 Suit와 Rank라는 두가지 열거형 타입을 포함한다.

블랙잭에서는 Ace카드 값이 1또는 11로 이러한 특징은 Values라는 구조체로 표현되며, Rank 열거형 안에 중첩되어 있다.

struct BlackjackCard {  // 구조체 생성

    // nested Suit enumeration
    enum Suit: Character {  // 구조체 내 열거형 생성, case들의 raw value가 Character이기에 ray type설정필수!
        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 {  // 열거형 안에 구조체 생성(struct ⊂ enum, enum ⊂ struct 이런형식임)
          // Ace는 first, second 두개의 값을 가지고 이외의 카드는 한가지 값만 가진다.
            let first: Int, second: Int?
        }

        // 열거형에서는 저장 프로퍼티는 만들 수 없지만, 연산 프로퍼티는 만들 수 있다는 점 잊지말자
        var values: Values {  // 연산프로퍼티, values라는 방금 만든 Values 구조체 타입의 프로퍼티 정의
            switch self {  // 여기서의 self는 Rank를 의미한다
            case .Ace:  // 각 케이스마다 Values 타입의 인스턴스를 리턴
                return Values(first: 1, second: 11)
            case .Jack, .Queen, .King:
                return Values(first: 10, second: nil)
            default:
            // Rank의 첫번째 case Two부터 2라는 값을 주고 있고, 따라서 나머지 케이스에는 1씩 증가한 값이 할당된다.
            // Three의 rawValue는 3이 된다 >> 리턴하는 Values의 인스턴스가 values에 할당되지 않는다(연산 프로퍼티 특징)
                return Values(first: self.rawValue, second: nil)
                }
        }
    }

    // BlackjackCard properties and methods
    // rank와 suit는 BlackjackCard 구조체의 저장 프로퍼티이다(위에 선언한 Rank, Suit 열거형 타입의)
    let rank: Rank, suit: Suit

    // description또한 BlackjackCard의 연산 프로퍼티
    var description: String {
      // 연산 프로퍼티 내 output 선언(output은 description안에서만 쓸수 있는 지역변수)
        var output = "suit is \(suit.rawValue),"  // suit.rawValue는 Suit열거형에서 나열한 기호들
            output += " value is \(rank.values.first)"  // values의 first값
            if let second = rank.values.second {  // 해당 카드가 ace카드였다면 if let(옵셔널 바인딩)으로 values에 second값 있는지 확인
                output += " or \(second)"  // second값이 있다면 넣어준다
            }
            return output
    }
}

이렇게 비로소 BlackjackCard 구조체를 정리해볼 수 있다. 그럼 진짜 BlackjackCard 구조체의 인스턴스를 생성해보자.

// BlackjackCard 타입의 theAceOfSpades라는 인스턴스 생성
let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)  // rank, suit는 BlackjackCard의 저장프로퍼티
print("theAceOfSpades: \(theAceOfSpades.description)")  // output을 리턴해주는 description 연산프로퍼티

// theAceOfSpades: suit is ♠, value is 1 or 11

theAceOfSpades: suit is ♠, value is 1 or 11

Rank와 Suit가 BlackjackCard에 중첩되어 있지만, 타입을 컨텍스트에서 유추할 수 있기에 인스턴스를 초기화하면 case 이름만으로 열거 case를 참조할 수 있게 된다.

Referring to Nested Types

정의 컨텍스트 외부에서 중첩된 타입을 사용하려면, 해당 이름앞에 nested type의 이름을 붙이면 된다.

let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// heartsSymbol is "♡"

따라서 BlackjackCard라는 인스턴스를 만들지 않고 해당값에 접근하고 싶다면, 위 코드처럼 nested type이름을 붙이면 된다.

swift 기본문법 - 자동참조 카운팅, ARC(Automatic Reference Counting)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고씩이머릿속-자동참조카운팅


자동참조 카운팅, ARC(Automatic Reference Counting)

메모리 사용을 관리하고 추적하기 위해 사용

대부분의 경우 메모리 관리는 ARC가 알아서 하고, 우리는 메모리 관리를 신경쓸 필요가 없다.
ARC는 클래스 인스턴스가 더이상 필요하지 않을 때 자동으로 그 인스턴스가 사용하는 메모리를 제거한다. 참조 카운팅은 클래스의 인스턴스에만 적용되며 구조체와 열거형은 참조 타입이 아니라 값 타입이라 해당사항이 없다.

ARC 작동원리

  • 클래스의 새로운 인스턴스를 생성할 때 마다 ARC는 인스턴스의 정보를 저장하기 위해서 메모리를 할당한다.
  • 메모리에는 해당 인스턴스와 연결된 모든 저장 프로퍼티의 값과 함께 인스턴스의 타입에 대한 정보가 저장된다.
  • 인스턴스가 필요 없어질 때, ARC 는 메모리를 다른 목적으로 사용될 수 있게 하기 위해 그 인스턴스가 사용하는 메모리를 해제한다.

이렇게 하면 클래스 인스턴스가 더 이상 필요하지 않을때 메모리 공간을 차지하지 않도록한다.(효율성 업!)

ARC는 해당 인스턴스에 대한 참조가 적어도 하나 이상 있다면, 인스턴스를 해제하지 않으며 이를 가능하게 하려면 클래스 인스턴스를 프로퍼티나 변수, 상수에 할당할 때마다 강한 참조(strong reference)를 만들어야 한다 ( 변수가 인스턴스를 꽉 붙잡고 있다고 생각하면 될듯. )

class Person { // Person 클래스에는 이름과 초기화 상태를 나타내는 초기화 함수와 초기화 해제 함수가 있음
   let name: String
   init(name: String) {
       self.name = name
       print("\(name) is being initialized")
   }
   deinit { // 인스턴스가 메모리 해제될 때 메세지 출력하게 하는 deinit
       print("\(name) is being deinitialized")
   }
}
// Person 클래스 타입의 세 변수 옵셔널로 생성. ARC 의 상태 변화 알아보기 위함
var reference1: Person? // 옵셔널 타입이므로 현재는 nil 로 초기화
var reference2: Person?
var reference3: Person?

reference1 = Person(name: "John Appleseed")// "John Appleseed is being initialized" 출력
// Person 인스턴스의 메모리가 생기는 시점
// referece1 에서 새로운 Person 인스턴스에 대한 강력한 참조가 생긴다
// 강한참조가 하나 이상 생겼기 때문에  ARC 는 이 인스턴스를 해제하지 않는다

reference2 = reference1
reference3 = reference1
// 같은 인스턴스 참조를 2, 3 에 모두 할당하면?
// 해당 인스턴스에 대한 강한 참조가 2개 더 생기는 것
// 그럼 현 시점에서는 Person 인스턴스에 대한 강한 참조는 3개 인 상태

reference1 = nil
reference2 = nil
// 2곳에 nil 할당해서 2개의 강한 참조를 끊으면?
// 아직 1개, 마지막 강한 참조가 남아있으므로 ARC 는 Person 인스턴스 메모리를 해제하지 않음
reference3 = nil // "John Appleseed is being deinitialized" 출력
// ARC 가 Person 인스턴스 메모리 해제시키는 시점
// 마지막 참조가 남아있는 reference3 에도 nil 할당하면, 인스턴스 안쓰는걸로 알고 ARC 가 메모리 없애버림

ARC와 GC의 차이

메모리 관리 기법 ARC GC
참조 카운팅 시점 컴파일 시 프로그램 동작 중
장점 컴파일 당시 이미 인스턴스의 해제 시점이 정해져 있어 인스턴스가 언제 메모리에서 해제될 지 예측 가능하다. 따라서 메모리 관리를 위한 시스템 자원을 추가할 필요가 없다 상호 참조 상황등의 복잡한 상황에서도 인스턴스를 해제할 수 있는 가능성이 더 높으며 특별히 규칙에 신경 쓸 필요가 없다.
단점 ARC의 작동 규칙을 모르고 사용하면 인스턴스가 영영 메모리에서 해제되지 않을 가능성이 있다 프로그램 동작 외의 메모리 감시를 위한 추가 자원이 필요해 한정적인 자원 환경에서 성능 저하가 발생할 수 있다. 더 나아가 명확한 규칙이 ㅇ벗기 때문에 인스턴스가 정확히 언제 메모리에서 해제될 지 에측이 어렵다.

강한 참조와 강한참조 순환 문제

인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어주는 것이 강한참조이다. 참조의 기본은 강한 참조이기 때문에 클래스 타입의 프로퍼티, 변수, 상수 등을 선언할 때 별도의 식별자를 명시하지 않으면 강한 참조가 된다.

class Person {
  let name: String
}

기본적으로 인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 강한 참조를 사용하면 참조 횟수가 +1 되며 강한 참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당해주면 그때 -1가 된다. 참조 횟수가 1이라도 남아있으면 메모리에서는 이를 해제하지 못하는데, 이렇게 강한 참조를 걸게 됨으로써 인스턴스가 메모리 해제되지 않아 계속된 메모리 누수를 만드는 경우가 있다. 그게 바로 강한 참조 순환 문제 이다.

class Person {
  let name: String

  init(name: String) {
    self.name = name
  }

  var room: Room?

  deinit {
    print("\(name) is being deinitialized")
  }
}

class Room {
  let number: String

  init(number: String) {
    self.number = number
  }

  var host: Person?

  deinit {
    print("Room \(number) is being deinitialized")
  }
}

var zehye: Person? = Person(name: "zehye")  // Person의 인스턴스 참조: 1
var room: Room? = Room(number: "22")  // Room의 인스턴스 참조: 1

room?.host = zehye  // Person의 인스턴스 참조: 2
zehye?.room = room  // Room의 인스턴스 참조: 2

zehye = nil  // Person의 인스턴스 참조: 1
room = nil   // Room의 인스턴스 참조: 1

zehye와 room에 nil을 할당하게 됨으로써 더이상 zehye 변수가 참조하던 Person클래스의 인스턴스에 접근할 방법도 room변수가 참조하던 Room 클래스의 인스턴스에 접근할 방법이 사라지게 되었다. 참조 횟수가 0이 되지않는 한, ARC의 규칙대로라면 인스턴스를 메모리에서 해제시키지 않기 떄문에 이렇게 두 인스턴스 모두 참조 횟수가 1이 남은체 메모리에 좀비처럼 남아있게 된다.

이를 해결하기 위한 방법으로 약한참조미소유참조 가 있다.

약한 참조

약한 참조는 강한 참조와는 다르게 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. 참조타입의 프로퍼티나 변수의 선언 앞에 weak 를 써주면 약한참조를 할 수 있다. 이는 참조횟수를 증가시키지 않기 떄문에 이스턴스를 강한 참조하던 프로퍼티나 변수에서 참조 횟수를 0으로 만들면 자신이 참조하던 인스턴스가 메모리에서 해제될 수 있다.

약한 참조는 상수에서 쓰일 수 없다. 자신이 참조하던 인스턴스가 메모리에서 해제된다면 nil이 할당될 수 있어야 하기 때문이다. 그래서 약한 참조를 할 때 자신의 값을 변경할 수 있는 변수로 선언해야하며 nil이 할당될 수 있어야 하기에 약한 참조는 항상 옵셔널이어야 한다. 즉 옵셔널 변수만이 약한참조를 할 수 있다.

class Room {
  let number: String

  init(number: String) {
    self.number = number
  }

  weak var host: Person?

  deinit {
    print("Room \(number) is being deinitialized")
  }
}

var zehye: Person? = Person(name: "zehye")  // Person의 인스턴스 참조: 1
var room: Room? = Room(number: "22")  // Room의 인스턴스 참조: 1

room?.host = zehye  // Person의 인스턴스 참조: 1
zehye?.room = room  // Room의 인스턴스 참조: 2

zehye = nil  // Person의 인스턴스 참조: 0, Room의 인스턴스 참조: 1
// zehye is being deinitialized
room = nil   // Room의 인스턴스 참조: 0
// Room 22 is being deinitialized

미소유 참조

약한 참조와 마찬가지로 미소유 참조 또한 인스턴스의 참조 횟수를 증가시키지 않는다. 그러나 미소유 참조는 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작한다. 즉, 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 스스로 nil을 할당해주지 않는다는 것을 의미한다. 따라서 미소유참조를 하는 변수나 프로퍼티는 옵셔널이나 변수가 아니어도 된다. 그러나 미소유 참조를 하면서 메모리에서 해제된 인스턴스에 접근하려 한다면 잘못된 메모리 접근으로 런타임 오류가 발생해 프로세스가 강제로 종료된다. 따라서 미소유 참조는 참조하는 동안 해당 인스턴스가 메모리에서 해제되지 않을 것이라는 확신이 있을때만 사용해야 한다.

이러한 미소유참조는 참조타입의 변수나 프로퍼티 앞에 unowned 키워드를 사용해주면 된다.

class Person {
  let name: String

  // Person은 카드를 소지할 수도 소지하지 않을 수도 있다.
  // 카드를 한번 가진 후 잃어버리면 안되기에 강한참조로!
  var card: Card?
  init(name: String) {
    self.name = name
  }

  deinit { print("\(name) is being deinitialized")}
}

class Card {
  let number: Int

  // 카드는 소유자가 분명히 존재해야한다.
  unowned let owner: Person
  init(number: UInt, owner: String) {
    self.number = number
    self.owner = owner
  }

  deinit { print("Card \(number) is being deinitialized")}
}

var zehye: Person? = Person(name: "zehye")  // Person 인스턴스 참조: 1

if let person: Person = zehye {
  // Card의 인스턴스 참조: 1
  person.card = Card(number: 11, owner: person)
  // Person 인스턴스 참조: 1
}

zehye = nil  // Person 인스턴스 참조: 0
// Card 인스턴스 참조: 0
// zehye is being deinitialized
// Card 11 is being deinitialized

swift 기본문법 - 접근 제한(Access Control)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


접근 제한(Access Control)

접근권한을 말한다.
class, struct, enum등의 type들을 비롯해 property. method, subscript 등 모두에게 접근제한을 부여할 수 있다.

이를 이해하기 위한 간단한 개념부터 봐보자.

Module

특정 기능을 하는 어플리케이션이나 프레임워크의 배포 단위로서 그냥 하나의 프로젝트와 연관된 모든 파일이라고 이해하면 된다. 예로 들어 가계부 앱을 만들었다고 하면, 이 가계부 앱과 관련된 XCode에서 작업하고 배포한 코드 파일 모두를 뜻한다.

Source File

프로젝트 파일 하나하나를 의미한다. 보통 한 Source File안에 하나의 class나 하나의 struct를 만드는 것이 일반적이지만 꼭 그럴필요는 없다.

Hiding Implementation Detail

어떤 함수나 class 인스턴스 등의 안에서 실행되는 코드를 외부에서는 알 수 없게 가린다는 의미이다. 이에 따라 밖에서는 안에 어떤 property나 method가 있는 지도 알 수 없으며, 이름을 알더라도 접근할 수 없게 만든다.

5가지의 Access Level

Swift에는 open, public, internal, fileprivate, private 5가지의 Access Level을 제공한다. 가장 위를 access level이 높다 또는 제한이 가장 없다고 하고, 아래로 갈수록 access level이 낮고 제한이 강하다고 표현한다. 주로 사용되는 것은 public, private 두개이다. internal은 모든 객체들이 특별히 access level을 지정해주지 않을 경우 가지는 default access level로서 작업하는 모듈 내에서만 접근 가능하도록 설정되어 있다.

open

가장 열린 Access Level. 외부의 다른 모듈이 import 해서 접근 가능하다. 심지어 import하는 외부 모듈안에서 subclass와 override도 가능하기 때문에 위험할 수도 있다. Open API 및 framework 구성시 활용한다.

public

open과 마찬가지로 외부 모듈이 import해서 접근 가능하지만, subclass와 override는 오로지 자신의 모듈 안에서만 가능하다. 이 또한 open API 및 framework 구성시 활용한다.

internal

자신의 모듈 안에서만 접근 가능하고 외부 모듈에서는 접근 불가하다. 한마디로 프로젝트 내부용이다. 모든 객체는 별도의 지정이 없으면 기본적으로 internal로 셋팅되며 Open API를 제공하지 않는 앱은 대체로 객체들을 internal로 두면 문제될 것이 없다. 다만, 다른 Source file이나 declaration 밖에서의 접근이 없다면 internal로 놔두는 것보다 private으로 두는 것이 더좋다.

fileprivate

같은 Source file안에서만 접근 가능하다. 즉, .swift라는 파일 안에서 fileprivate인 함수가 있다면 해당 swift파일안에서는 접근이 가능하지만, 같은 프로젝트의 다른 swift 파일에서는 접근이 불가능하다.

private

가장 폐쇄된 Access Level. 같은 declaration 공간({})안에서만 접근가능하다. 즉, 함수 안에서 private으로 선언된 변수는 해당 함수안에서, class안에서 private으로 선언된 변수는 해당 class 안에서만 접근이 가능하다.

swift 기본문법 - 서브스크립트(Subscript)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


서브스크립트(Subscript)

컬렉션, 리스트, 시퀀스 타입의 개별 요소에 접근할 수 있는 지름길을 제공하는 것

let array = [1, 2, 3]

위와 같은 배열이 있을때 배열 내부의 2라는 값을 얻기 위해 array[1] 이라는 문법을 사용한다.

여기에서 array [1] 이 부분이 바로 서브스크립트이다. 즉, 원하는 값을 쉽게 찾기 위해 정의하는 문법을 서브스크립트라고 한다.
array가 서브스크립트 문법을 구현하지도 않았는데 사용할 수 있는 이유는 스위프트 표준 라이브러리에 정의된 array 구조체 내부에 서브스크립트가 이미 구현되어 있기 때문이다.

구현해보기

  • subscript 키워드를 사용
  • 연산 프로퍼티에서 사용했던 get, set 키워드 사용
subscript(index: Int) -> Int {
    get {
        // return an appropriate subscript value here
    }
    set(newValue) {
        // perform a suitable setting action here
    }

예제

1.구구단

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index  // subscript에 입력한 정수만큼 곱하기, set없이 읽기전용
    }
}

let threeTimesTable = TimesTable(multiplier: 3)  // 구구단 3단
print("six times three is \(threeTimesTable[6])")  // 18

2.영화 리스트

class MovieList{
    private var tracks = ["s", "d", "r"]
    subscript(index: Int) -> String {
        get {
            return self.tracks[index]
        }
        set {
            self.tracks[index] = newValue
        }
    }
}
 var movieList = MovieList()
print("영화리스트에서 두번째 영화는: \(movieList[1])")  // d