Swift: Struct vs Class vs Actor, Value vs Reference Types, Stack vs Heap | Swift Concurrency #8
struct CustomStruct {
var title: String
// Default Initializer from Swift
}
struct CustomStruct2 {
var title: String
func makeNewStruct(_ title: String) -> CustomStruct2 {
return CustomStruct2(title: title)
}
mutating func updateTitle(_ title: String) {
self.title = title
}
}
struct CustomStruct3 {
private var title: String
init(title: String) {
self.title = title
}
func getTitle() -> String {
return title
}
mutating func setTitle(_ title: String) {
self.title = title
}
}
class CustomClass {
var title: String
init(title: String) {
self.title = title
}
func updateTitle(_ title: String) {
self.title = title
}
}
class CustomClass2 {
var title: String
init(title: String) {
self.title = title
print("\(title) is initiated")
}
deinit {
print("\(title) is deinitiated")
}
}
actor CustomActor {
var title: String
// to be updated
init(title: String) {
self.title = title
}
func updateTitle(_ title: String) {
self.title = title
}
}
extension StructClassActorBootCamp {
private func runTest() {
actorTest()
}
private func structTest1() {
let objectA = CustomStruct(title: "Starting Title")
print("ObjectA : \(objectA.title)")
print("Pass the VALUE of objectA to objectB")
var objectB = objectA
print("ObjectB : \(objectB.title)")
// ObjectA : Starting Title
// ObjectB : Starting Title
objectB.title = "Second Title"
// let title -> var title, let objectB -> var objectB. complier changes it
print("ObjectB title changed")
print("ObjectA : \(objectA.title)")
print("ObjectB : \(objectB.title)")
// ObjectA : Starting Title
// ObjectB : Second Title -> ObjectA <-> ObjectB not referencing themselves. (: Value Type)
}
private func classTest1() {
let objectA = CustomClass(title: "Starting Title")
print("ObjectA : \(objectA.title)")
print("Pass the REFERENCE of objectA to objectB")
let objectB = objectA
print("ObjectB : \(objectB.title)")
// ObjectA : Starting Title
// ObjectB : Starting Title
objectB.title = "Second Title"
// let title -> var title. complier changes it: not 'let' objectB.
print("ObjectB title changed")
print("ObjectA : \(objectA.title)")
print("ObjectB : \(objectB.title)")
// ObjectA : Second Title
// ObjectB : Second Title -> ObjectA === ObjectB referencing same place (: Reference Type)
print(objectA === objectB)
// true
}
}
mutating
키워드를 통해 self
를 바꾼다는 것을 명시해야 함mutating
키워드가 없음. 즉 그대로 변경 가능하다는 뜻. 특정 텍스트 인스턴스를
let
, 상수로 받은 뒤 인스턴스의 변수 로 선언한 저장 프로퍼티 값을 변경 가능한 까닭:let
이라는 상수는 곧 해당 클래스를 가리키고 있는 포인터가 변하지 않는다는 것을 의미할 뿐, 내부의 값은 (변수로 선언되었다면) 주소값을 통해 접근, 변경 가능하기 때문
extension StructClassActorBootCamp {
private func structTest2() {
var struct1 = CustomStruct(title: "Title1")
print("Struct1: \(struct1.title)")
struct1.title = "Title2"
print("Struct1: \(struct1.title)")
// Struct1: Title1
// Struct1: Title2
var struct2 = CustomStruct2(title: "Title1")
print("Struct2 : \(struct2.title)")
struct2 = CustomStruct2(title: "Title2")
// totally new struct assigned
print("Struct2 : \(struct2.title)")
// struct2.title : Title1
// struct2.title : Title2
struct2 = struct2.makeNewStruct("New Title")
print("Struct2 : \(struct2.title)")
// struct2.title : New Title
// Totally new Structure
struct2.updateTitle("Title3")
// Mutating function
print("Struct2 : \(struct2.title)")
// struct2.title : Title3
}
private func structTest3() {
var struct1 = CustomStruct3(title: "Title1")
var title = struct1.getTitle()
print(title)
// Title1
struct1.setTitle("Title2")
title = struct1.getTitle()
print(title)
// Title2
// get, set method to handle data inside struct
}
}
extension StructClassActorBootCamp {
private func classTest2() {
let class1 = CustomClass(title: "Title1")
print("Class1 : \(class1.title)")
// Class1 : Title1
class1.updateTitle("Title2")
print("Class1 : \(class1.title)")
// Class1 : Title2
// Does not have to set "mutating" keyward inside Class
}
}
mutating
키워드를 통해 변수로 선언한 저장 프로퍼티 값을 바꾸는 방법 (2). 새로운 구조체를 할당, 변수로 선언된 특정 값을 덮어 씌우는 방법
mutating
이 곧inout
으로 바꾸는 것과 같은 맥락인 것 같다!
extension StructClassActorBootCamp {
private func actorTest() {
Task {
// Need to get async -> Call them inside 'Task' block
var actor1 = CustomActor(title: "Actor1")
await print("Actor1 title : \(actor1.title)")
// Actor1 title : Actor1
// actor1.title = "Actor2" -> Ban
// Actor-isolated property 'title' can not be mutated from a non-isolated context
await actor1.updateTitle("Actor2")
await print("Actor1 title : \(actor1.title)")
// Actor1 title : Actor2
}
}
}
actor
내부의 프로퍼티는 actor-isolated
프로퍼티이기 때문에 일반적인 방법으로는 변경 불가능 → actor
클래스 내부에서 선언한 함수를 통해 self
접근, 특정 프로퍼티 값을 변경 가능 → await
를 통해 비동기 동작이 끝났는지 확인할 수 있고 이를 Task
내부에서 실행mutated
)ObservableObject
등 참조 방식을 통해 동일한 값의 현재 상태 감지 보장(뷰 모델, 매니저 클래스 등)await
를 통해 동기화 보장 가능URLSession.shared
등) 적절. 다양한 곳에서 접근 가능한 클래스, 동기화 자동으로 보장 (await
)구조체는 클래스와 달리 상속이 불가능하다. 하지만 프로토콜 + 익스텐션 활용을 통해
deinit
을 통해 직접 확인 가능), 효율적 메모리 사용이 가능class StrongClass1 {
var title: String
var strongClass2: StrongClass2?
var weakClass: WeakClass?
init(title: String, strongClass2: StrongClass2? = nil, weakClass: WeakClass? = nil) {
self.title = title
self.strongClass2 = strongClass2
self.weakClass = weakClass
print("\(title) is initiated")
}
deinit {
print("\(title) is deinitiated")
}
}
class StrongClass2 {
var title: String
var strongClass1: StrongClass1?
init(title: String, strongClass1: StrongClass1? = nil) {
self.title = title
self.strongClass1 = strongClass1
print("\(title) is initiated")
}
deinit {
print("\(title) is deinitiated")
}
}
class WeakClass {
var title: String
weak var strongClass1: StrongClass1?
init(title: String, strongClass1: StrongClass1? = nil) {
self.title = title
self.strongClass1 = strongClass1
print("\(title) is initiated")
}
deinit {
print("\(title) is deinitiated")
}
}
extension StructClassActorBootCamp {
private func classTest3() {
var class1: CustomClass2?
class1 = CustomClass2(title: "New Class")
// New Class is initiated
// Reference Count : 1
class1 = nil
// Reference Count : 0 -> Deinit. Deallocated from Memory
// New Class is deinitiated
}
private func classTest4() {
var class1:StrongClass1? = StrongClass1(title: "Strong Class1")
var class2:StrongClass2? = StrongClass2(title: "Strong Class2")
// Strong Class1 is initiated
// Strong Class2 is initiated
class1?.strongClass2 = class2
class2?.strongClass1 = class1
class1?.strongClass2 = nil
class2?.strongClass1 = nil
class1 = nil
class2 = nil
// Strong Class1 is deinitiated
// Strong Class2 is deinitiated
class1 = StrongClass1(title: "Strong Class1")
class2 = StrongClass2(title: "Strong Class2")
// Strong Class1 is initiated
// Strong Class2 is initiated
class1?.strongClass2 = class2
class2?.strongClass1 = class1
class1 = nil
class2 = nil
// Still Strong reference cycle retained -> Memory leaking
}
private func classTest5() {
var class1:StrongClass1? = StrongClass1(title: "Strong Class1")
var class2:WeakClass? = WeakClass(title: "Weak Class")
// Strong Class1 is initiated
// Weak Class is initiated
class1?.weakClass = class2
class2?.strongClass1 = class1
class1 = nil
class2 = nil
// Strong Class1 is deinitiated
// Weak Class is deinitiated
// strongClass1 inside class2(Weak Class instance) -> nil, then ARC -> 0, then class1 -> deinit
// weak reference -> make its non-having strong reference less than strong reference
}
}
nil
이 될 참조'임을 미리 알려주기 때문에 메모리 누수에 대한 걱정을 덜 수 있음nil
할당을 통한 카운팅을 줄이는 방법을 통해 ARC를 조정 가능, 강한 참조 및 약한 참조를 함께 씀으로써 고리를 끊을 수 있다는 것 역시 주의@StateObject
로 뷰에서 관찰하고 있는 ObservableObject
는 뷰 리렌더링 도중에도 값을 계속해서 유지. @ObjservedObject
로 관찰하고 있다면 Deinit
됨actor
클래스를 통해 동기화 보장 가능import SwiftUI
actor StructClassActorBootCampDataManager {
init() {
print("StructClassActorBootCampDataManager Init")
}
deinit {
print("StructClassActorBootCampDataManager Deinit")
}
func getDataFromDatabase() {
}
}
class StructClassActorBootCampViewModel: ObservableObject {
@Published var title: String = ""
let dataManager: StructClassActorBootCampDataManager
init(dataManager: StructClassActorBootCampDataManager) {
self.dataManager = dataManager
print("StructClassActorBootCampViewModel Init")
}
deinit {
print("StructClassActorBootCampViewModel Deinit")
}
}
struct StructClassActorBootCamp: View {
@StateObject private var viewModel = StructClassActorBootCampViewModel(dataManager: StructClassActorBootCampDataManager())
// -> ObservableObject class: Init ...
// Even if this entire struct View would be re-rendered, its StateObject would be same
// @ObservedObject private var viewModel = StructClassActorBootCampViewModel()
// -> ObservableObject class: Init and Deinit ...
let isActive: Bool
init(isActive: Bool) {
self.isActive = isActive
print("View Init, isActive : \(isActive)")
/*
View Init, isActive : true
View Init, isActive : false
View Init, isActive : true
View Init, isActive : false
... -> Whenever isActive toggled, this View has been initiated.
*/
}
var body: some View {
Text(viewModel.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
.background(isActive ? .red : .blue)
.onAppear {
runTest()
}
}
}
struct StructClassActorBootCampHomeView: View {
@State private var isActive: Bool = false
var body: some View {
StructClassActorBootCamp(isActive: isActive)
.onTapGesture {
isActive.toggle()
}
}
}
HomeView
의 자식 뷰인 StructClassActorBootCamp
뷰는 이니셜라이저에 필요한 파라미터 isActive
값이 바뀜에 따라 계속해서 재생성 → 뷰 리렌더링StructClassActorBootCampViewModel
ObservableObject
는 @StateObject
로 관찰되고 있기 때문에 뷰 렌더링이 일어난다 하더라도 초기 Init
이후 Deinit
되지 않고 있음 → 기존에 참조한 값을 그대로 유지할 수 있음