본문 바로가기

학습 노트/Swift (2021)

118 ~ 121. Method and Subscript (메소드와 서브스크립트)

Instance Method (인스턴스 메소드)

메소드는 특징 형식에 속한 함수이다.

Syntax

func name(parameters) -> ReturnType {
    code
}

instance.method(parameter)

구현 위치와 인스턴스를 통해 호출한다는 점을 빼면 함수와 동일하다.
클래스, 구조체, 열거형에서 구현 할 수 있다.
인스턴스 메소드는 특성 인스턴스와 연관된 기능을 구현한다.

class Sample {
	var data = 0
	static var sharedData = 123
	
	func somethig() {
		print(data)
	}
	
	func call() {
		somethig()
	}
}

Sample.something 메소드는 Sample.data 속성을 호출하지만
Sample.call 메소드는 다른 인스턴스 메소드인 Sample.something 메소드를 호출한다.

이렇게 인스턴스 메소드에서 다른 멤버에 접근 할 때는 self를 생략 할 수 있다.

class Sample {
	var data = 0
	static var sharedData = 123
	
	func somethig() {
		print(data)
		Sample.sharedData
	}
	
	func call() {
		somethig()
	}
}

단, sharedData 같은 타입 멤버에 접근 할 때에는 형식 이름을 통해 접근한다.

class Sample {
	var data = 0
	static var sharedData = 123
	
	func somethig() {
		print(data)
		Sample.sharedData
	}
	
	func call() {
		somethig()
	}
}

//-----

let a = Sample()
a.data
a.somethig()
a.call()

메소드를 호출 할 때는 멤버에 접근 할 때와 같이
인스턴스의 이름을 통해 접근해야 한다.

class Size {
	var weight = 0.0
	var height = 0.0
	
	func increase() {
		weight += 1
		height += 1
	}
}

let b = Size()
b.increase()
결과

weight 1
height 1

이번엔 Size 클래스를 만들고, 멤버인 높이와 무게를 1씩 초기화 하는 메소드를 만들고 결과를 확인 했다.
원하는 대로 초기값 보다 1 증가한 결과를 반환하고 있지만, 이런 경우 주의해야 한다.
클래스를 구조체로 바꿔 보자.

struct Size {
	var weight = 0.0
	var height = 0.0
	
	func increase() {
		weight += 1
		height += 1
	}
}

let b = Size()
b.increase()
결과

//error

클래스와 구조체의 문법은 거의 동일하며, 대부분 호환도 가능하다.
하지만 메소드의 선언 부분에서 오류가 발생하는 것을 확인 할 수 있다.
클래스에서는 메소드가 값을 자유롭게 변경 할 수 있지만 구조체에서는 mutaing으로 선언된 메소드만 값을 변경 할 수 있다.

struct Size {
	var weight = 0.0
	var height = 0.0
	
	mutatingfunc increase() {
		weight += 1
		height += 1
	}
}

let b = Size()
b.increase()
결과

//error

하지만 이번엔 호출부분에서 에러가 발생한다.
이전에 정리했듯 값형식 인스턴스는 인스턴스의 속성이 멤버의 속성에도 영향을 끼친다.

이 두가지 문제는 구조체와 클래스의 데이터 처리 방식의 차이인데,
구조체는 값형식, 클래스는 참조형식으로 앞으로도 중요한 부분이다.
(https://chillog.tistory.com/48)

따라서 코드를 다음과 같이 수정한다.

struct Size {
	var weight = 0.0
	var height = 0.0
	
	mutating func increase() {
		weight += 1
		height += 1
	}
}

var b = Size()
b.increase()
결과

weight 1
height 1

이젠 구조체에서 선언 된 메소드가 클래스에서 선언 됐을 때와 동일한 기능을 하는 것을 볼 수 있다.

 

Type Method (타입 메소드)

속성이 인스턴스 속성과 타입 속성으로 구분 되는 것과 마찬가지로,
메소드도 동일하게 구분 된다.

타입 메소드는 인스턴스가 아닌 형식에 관련된 메소드이다.

Syntax

static func name(parameters) -> ReturnType {
    statements
}

class func name(parameters) -> RetrunType {
    statements
}

Type.method(parameters)

인스턴스 메소드와 마찬가지로 구조체, 클래스, 열거형에서 모두 사용 할 수 있다.
인스턴스 메소드와는 func 키워드 앞에 static과 class가 분는 다는 점이다.
class 키워드는 서브클래스에서 오버라이딩을 허용할 때 사용한다.

class Circle {
	static let pi = 3.14
	var radius = 0.0
	
	func area() -> Double {
		return radius * radius * Circle.pi
	}
}

원의 반지름과 파이를 이용해 넓이를 구하는 메소드를 포함한 Circle 클래스를 생성한다.
Circle.area는 전달 되는 parameter가 없으며 결과로 Double을 반환한다.
또한, 코드에서 보다싶이 일반 인스턴스 속성은 이름 만으로 접근할 수 있지만, static으로 선언 된 타입 속성은 타입 이름을 통해 별도로 접근해야 한다.

class Circle {
	static let pi = 3.14
	var radius = 0.0
	
	func area() -> Double {
		return radius * radius * Circle.pi
	}
	
	static func printAnswer() {
		print(pi)
	}
}

하지만 static 키워드로 선언된 타입 메소드인 Circle.printArea는 별도의 타입 이름 없이 즉시 접근 가능하다.
하지만 인스턴스 멤버에 접근할 수 있는 방법은 없다.

class Circle {
	static let pi = 3.14
	var radius = 0.0
	
	func area() -> Double {
		return radius * radius * Circle.pi
	}

	static func printPi() {
		print(pi)
	}
}

Circle.printPi()
결과

3.14

인스턴스 이름으로 접근하지 않기 때문에 따로 인스턴스를 생성하지 않아도 사용 할 수 있다.
이번엔 Circle을 상송 받는 Stroke 클래스를 생성한다.

class Circle {
	static let pi = 3.14
	var radius = 0.0
	
	func area() -> Double {
		return radius * radius * Circle.pi
	}
	
	static func printPi() {
		print(pi)
	}
}

//-----

class Stroke: Circle {
	override static func printPi() {
		print(pi)
	}
}
결과

//error

ststic으로 선언 된 printPi 메소드는 상속 받을 수 없다.
이는 인스턴스 메소드, 타입 메소드 모두 동일하다.
상속과 오버라이드는 추후에 언급한다.

class Circle {
	static let pi = 3.14
	var radius = 0.0
	
	func area() -> Double {
		return radius * radius * Circle.pi
	}
	
	class func printPi() {
		print(pi)
	}
}

//-----

class Stroke: Circle {
	override static func printPi() {
		print(pi)
	}
}

static 키워드를 class로 바꾸면 정상적으로 오버라이드 가능해 진다.

 

Subscript (서브스크립트)

Syntax

instance[index]
instance[key]
instance[range]

콜렉션을 배울 때 언급 됐던 서브스크립트이다.

let list = ["A", "B", "C"]
list[0]
결과

"A"

A, B, C 세개의 캐릭터가 저장 되는 배열을 만들고
첫번째 인덱스에 접근한다.
이 때 사용한 것이 서브스크립트다.

인스턴스 이름 다음에 '[ ]' 그리고 그 사이에 인덱스, 범위, 키 가 들어간다면 모두 서브스크립트이다.

서브스크립트 선언

기존에 사용하던 서브스크립트 들은 모두 이미 만들어진 서브스크립트였다.
이번엔 직접 서브스크립트를 선언한다.

Syntax

subscript(parameters) -> ReturnType {
    get {
        return expression
    }
    set(name) {
        statements
    }
}

형식은 메소드와 유사하다.
서브스크립트의 '[ ]' 사이에 전달되는 값들은 parameter에 정의한다.
이 때 parameter의 형식은 자유로우나 보통 두 개 이하의 parameter를 사용하고.
입출력 parameter를 사용하거나 기본값을 지정하는 것은 불가능하다.
생략하는 것 또한 불가능하다.

retrunType은 서브스크립트의 반환 형심임과 동시에 저장하는 값의 형식이다.
따라서 returnType 또한 생략 할 수 없다.

getter와 setter는 계산 속성과 유사하다.
둘을 모두 선언하면 서브스크립트를 통해 값을 읽고 쓸 수 있다.
서브스크립트로 저장한 값은 parameter를 통해 setter로 전달 된다.
parameter 이름을 직접 선언하고자 한다면 setter의 '( )' 안에서 선언한다.
이를 생략하면 newValue가 기본값으로 생성 된다.
읽기 전용으로 선언하려는 경우 setter를 제외하고 getter만 선언한다.
getter와 setter 중 getter는 반드시 선언 되어야 한다.

class List {
	var data = [1, 2, 3]
	
	subscript(index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

List 클래스는 data의 정수 배열을 갖고, Int를 받아 Int를 반환하는 서브스크립트를 가진다.

class List {
	var data = [1, 2, 3]
	
	subscript(index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

//-----

let t = List()
t[0]
결과

1

정수형의 값을 전달하면 getter를 통해 해당 값과 일치하는 인덱스의 데이터를 반환한다.

class List {
	var data = [1, 2, 3]
	
	subscript(index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

//-----

let t = List()
t[1] = 123
결과

123
[1, 123, 3]

setter를 통해 두 번요소가 업데이트 된다.

class List {
	var data = [1, 2, 3]
	
	subscript(index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

//-----

let t = List()
t[0, 1]
결과

//error

parameter 두개를 요구하는 서브스크립트가 없기 때문에 에러가 발생한다.
정수가 아닌 값을 전달해도 에러가 발생한다.

함수와 메소드에서는 parameter를 선언 할 때 이름을 하나만 지정하면 이 이름은 argument 이름과 parameter 이름으로 동시에 사용된다.
하지만 이는 서브스크립트에선 적용되지 않는다.
만약 argument 이름이 필요하다면 직접 선언해야 할 필요가 있다.

class List {
	var data = [1, 2, 3]
	
	subscript(i index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

var t = List()
t[1] = 123
결과

//error

이렇게 parameter 이름 앞에 argument 이름을 선언하면 에러가 발생한다.
이는 이후 인스턴스에서 argument 이름을 사용하지 않았기 때문으로 다음과 같이 수정하면 정상적으로 동작한다.

class List {
	var data = [1, 2, 3]
	
	subscript(i index: Int) -> Int {
		get {
			return data[index]
		}
		set {
			data[index] = newValue
		}
	}
}

var t = List()
t[i: 1] = 123

다만 이렇게 argument 이름을 사용하는 경우는 parameter의 수가 두 개 이상이고,
이러한 parameter들의 가독성을 높히는 경우 제한적으로 사용한다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			return data[x][y]
		}
	}
}

var y = Matrix()
y[2, 2]
결과

9

이번엔 구조체에 서브스크립트를 선언했다.
Matrix 구조체는 3*3의 2차원 배열을 가지고,
선언한 서브스크립트는 두 개의 정수형 데이터를 받아 배열 내의 값으 반환하는 서브스크립트이다.
setter를 선언하지 않았으므로

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			return data[x][y]
		}
	}
}

var y = Matrix()
y[1,2] = 20
결과

//error

위와 같이 데이터를 저장하는 동작은 수행할 수 없다.

연습

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			return data[x][y]
		}
	}
}

위에서 만든 2차원 배열을 가지는 구조체의 코드이다.
이 코드는 한 가지 문제가 있는데,
잘못된 범위를 전달하면 오동작을 하게 된다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			return data[x][y]
		}
	}
}

let y = Matrix()
y[0, 7]
결과

//Fatal error

페이탈 에러는 크래쉬의 주범이므로 이를 우회할 방법을 마련해야 한다.
우선 전달되는 값의 범위 유효성을 판단해야 한다.
따라서 if문을 사용해 해당 조건을 만든다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			if {
			}
			else {
			}
		}
	}
}

이 때 검산 방법에 대한 고민이 필요하다.
다차원 배열의 크기를 구하는 방법으로 .count 속성을 제공한다.
해당 속성은 이전에 정리한 적이 있었는데, 다차원 배열에 적용하는 것은 이번이 처음이다.
우선 새로운 배열을 만들어 count 속성을 적용해 본다.

let v = [[1, 2, 3], [4, 5, 6]]
v.count
결과

2

기대와 다르게 2*3 배열의 x만 반환 된 것을 볼 수 있다.
따라서 x값은 일반적인 count 속성으로 비교하면 된다.
이제 y값을 비교할 방법을 찾아야 하는데, 이는 배열에 접근하는 방식을 떠올리면 간단하다.
배열에 접근 할 때는 name[x][y]꼴로 접근하게 되는데,
이 때 y의 최대 값이 공통이라는 것을 떠올리면 된다.
따라서 우리는 v[x].count 를 사용하게 되면 y의 값을 구할 수 있다.

let v = [[1, 2, 3], [4, 5, 6]]
v[0].count
결과

3

따라서 우리의 조건문은 다음과 같이 완성 된다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			if x <= data.count && y <= data[x].count {
			}
			else {
			}
		}
	}
}

x의 값이 배열의 값과 비교했을 때 유효하고, y의 값이 유효하면 해당 코드를 진행하고,
이외의 경우엔 분기한다.
따라서 진행 될 코드에는 반환을 진행 해 주면 된다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			if x <= data.count && y <= data[x].count {
				return data[x][y]
			}
			else {
			}
		}
	}
}

이외의 경우엔 코드를 끝내거나 값이 존재하지 않는 다는 것을 알려야 한다.
따라서 nil을 반환하기로 결정했고

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int {
		get {
			if x <= data.count && y <= data[x].count {
				return data[x][y]
			}
			else {
				return nil
			}
		}
	}
}

nil을 전달 하려면 반환형은 optional이 되어야 한다.

struct Matrix {
	var data = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
	
	subscript(x: Int, y: Int) -> Int? {
		get {
			if x <= data.count && y <= data[x].count {
				return data[x][y]
			}
			else {
				return nil
			}
		}
	}
}

var y = Matrix()
y[2, 2]
y[1,7]
결과

9
nil

코드가 의도한 대로 잘 작동하는 것을 확인 할 수 있다.

 

Dynamic Member Lookup

해당 문법은 python과의 호환을 위해 도입된 문법이다.
점문법을 통해 서브스크립트에 접근하는 단축 문법을 제공한다.

해당 문법의 핵심은 @dynamicMemberLookup 특성이다.

@dynamicMemberLookup
struct Person {
	var name: String
	var address: String
}

일단은 해당 특성을 클래스나 구조체 등 위에 작성하는 것으로 해당 특성을 지원하게 된다.

이후 서브스크립트 생성 과정이 필요하다.

@dynamicMemberLookup
struct Person {
	var name: String
	var address: String
	
	subscript(dynamicMember member: String) -> String {
		switch member {
		case "nameKey":
			return name
		case "addressKey":
			return address
		default:
			return "not available"
		}
	}
}

let p = Person(name: "Tom", address: "Busan")
p.name
p.address
p.[dynamicMember: "nameKey"]
p.[dynamicMember: "addressKey"]
결과

"Tom"
"Busan"
"Tom"
"Busan"

서브스크립트는 하나의 parameter를 받아야 하고,
argument 이름은 반드시 'dynamicMember'으로,
parameter의 형식은 String으로 선언해야 한다.
이 때, 반환형은 자유롭게 설정 가능하다.

서브스크립트를 통해서도, 속성으로도 쉽게 접근 할 수 있다.

@dynamicMemberLookup
struct Person {
	var name: String
	var address: String
	
	subscript(dynamicMember member: String) -> String {
		switch member {
		case "nameKey":
			return name
		case "addressKey":
			return address
		default:
			return "not available"
		}
	}
}

let p = Person(name: "Tom", address: "Busan")

//-----

p.nameKey
p.addressKey
p.wrong
결과

"Tom"
"Busan"
"not available"

하지만 dynamic member lookup은 argument 이름과 서브스크립트 문법을 벗어난 점문법으로의 접근도 허용한다.

dynamic member lookup은 코드의 유연성을 증가시킨다.
하지만 대상에 접근하는 시점이 runtime인 데 따른 단점 또한 존재한다.
컴파일러는 접근 가능한 서브스크립트 여부를 판단하지 못한다. 따라서 자동완성을 지원하지 않는다. 또한, 오타로 인한 에러를 미리 판단하지 못한다.
따라서 가독성과 유지보수에는 단점이 명확하다.

 


Log

2021.09.11.
블로그 이전으로 인한 글 옮김 및 수정