IOS面试题——Swift结构体,类,枚举(3)

一 面试题汇总

  1. 枚举是否可以递归?indirect
  2. 枚举值原始值和附加值分别是什么?内存占用怎么计算?
  3. 结构体内存占用如何计算?
  4. 结构体自定义初始化方法和自动生成的初始化方法有什么关系?
  5. 结构体能否继承?如果改变property,需要怎么做?mutating
  6. 类自动生成的初始化方法与结构体自动初始化方法有何区别?
  7. struct 与 class有什么区别?值类型和引用类型,继承,初始化方法,属性值改变
  8. 如何给结构体,类,枚举增加subscript下标?subscript可以用来做什么?

二 面试题解答(仅供参考)

2.1 枚举是否可以递归?indirect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在 Swift 中,枚举类型是可以递归的。
这意味着一个枚举的 case 可以包含该枚举类型本身作为关联值,从而形成递归结构。

为了允许枚举类型递归,需要在枚举定义中使用 indirect 关键字来标记枚举的关联值是递归的。

indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// 用递归结构表示一个数学表达式:(5 + 4) * 2
let expression = ArithmeticExpression.multiplication(
ArithmeticExpression.addition(
ArithmeticExpression.number(5),
ArithmeticExpression.number(4)
),
ArithmeticExpression.number(2)
)

在这个示例中,ArithmeticExpression 枚举类型定义了一个递归结构,
其中的 addition 和 multiplication case 都包含了ArithmeticExpression 类型作为其关联值。
通过使用 indirect 关键字,我们告诉编译器允许枚举的关联值是递归的。

递归枚举在处理一些树形结构、表达式解析、数据结构等场景下非常有用。
通过使用递归枚举,可以清晰地表达出递归结构,从而提高代码的可读性和可维护性。

2.2 枚举值原始值和附加值分别是什么?内存占用怎么计算?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
原始值是枚举类型固定的预定义值。原始值必须是相同的类型,并且必须在声明枚举时确定。
每个原始值在内存中都只存储一次。

1-内存占用计算:
1.1-如果原始值是整数型(如 Int、UInt),则占用的内存大小与该整数类型相同。
1.2-如果原始值是其他固定大小的类型(如 Float、Double、Character 等),则占用的内存大小与这些类型的大小相同。
1.3-如果原始值是可变大小的类型(如 String、Array、Dictionary 等),则枚举实例将存储一个指向该值的引用,而不是直接存储原始值本身。

2-关联值(Associated Values):
关联值是与枚举实例关联的实际数据。每个枚举实例的关联值的大小和类型可能会不同。
关联值可以是任意类型,包括基本类型(如 Int、String 等)、结构体、枚举和类等。

内存占用计算:
对于基本类型的关联值,占用的内存大小与该类型的大小相同。
对于结构体、枚举和类等自定义类型的关联值,占用的内存大小取决于该类型的成员变量和引用类型的大小。

总的来说,原始值在内存中通常会比关联值占用更少的内存空间,
因为原始值的大小是固定的,而关联值的大小可能是动态的。
同时,关联值更加灵活,可以包含更复杂的数据结构

2.3 结构体内存占用如何计算?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在 Swift 中,结构体(Structures)是一种值类型,它们在内存中存储其成员变量的值。
计算结构体的内存占用涉及考虑结构体成员变量的大小及对齐方式。
以下是计算结构体内存占用的一般步骤:

1-计算成员变量大小:首先,计算结构体中每个成员变量所占的内存大小。
对于基本类型(如 Int、Double、Bool 等)和固定大小的类型(如 Float、Character 等),
它们的大小通常是固定的。对于引用类型(如 String、Array、Dictionary 等),
则需要考虑其引用大小,因为它们存储的是引用而不是实际数据。

2-考虑对齐方式:计算机在存储数据时通常会按照某种对齐方式进行存储,以提高访问效率。
对于结构体,通常会按照其成员变量中最大的成员变量的大小进行对齐,以确保每个成员变量的地址都是对齐的。
对齐方式可以通过 MemoryLayout 来获取。

3-计算填充字节:由于对齐的存在,可能会出现填充字节,即某些成员变量后面会补充一些空白字节,
以确保下一个成员变量的地址是对齐的。
填充字节的大小取决于对齐方式和结构体成员变量的排列顺序。

4-计算总大小:最后,将所有成员变量的大小相加,再加上填充字节的大小,即得到结构体的总内存占用大小。

值得注意的是,对于嵌套的结构体,还需要递归地计算其内部结构体的内存占用。
此外,内存占用的大小还可能会受到编译器和目标平台的影响。
因此,具体的内存占用计算可能会有所不同。

2.4 结构体自定义初始化方法和自动生成的初始化方法有什么关系?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
在 Swift 中,结构体(Structures)默认会自动生成一个成员逐一初始化方法(Memberwise Initializer),
该初始化方法可以用来初始化结构体的所有成员变量。

1-自动生成的初始化方法(成员逐一初始化方法):
当你定义了一个结构体时,Swift 会自动生成一个成员逐一初始化方法,
其参数列表包含了结构体中每个成员变量的参数,
可以通过这个初始化方法来为结构体的每个成员变量赋值。

struct MyStruct {
var property1: Int
var property2: String
}

let instance = MyStruct(property1: 10, property2: "Hello")

在这个例子中,MyStruct 结构体默认会有一个自动生成的初始化方法,
该方法的参数列表包含了 property1 和 property2 两个参数,
用于对结构体的成员变量进行初始化。

2-自定义初始化方法:
你也可以为结构体定义自定义的初始化方法,以满足特定的需求。
自定义初始化方法可以提供额外的参数,并在初始化过程中执行一些特定的逻辑操作。

struct MyStruct {
var property1: Int
var property2: String

init(customProperty1: Int, customProperty2: String) {
self.property1 = customProperty1
self.property2 = customProperty2
}
}

let instance = MyStruct(customProperty1: 20, customProperty2: "World")

2.5 结构体能否继承?如果改变property,需要怎么做?mutating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在 Swift 中,结构体(Structures)是一种值类型,而不是引用类型。
与类不同,结构体是不能继承的,因为它们不支持类的继承机制。

1-不能继承的特性:
1.1-不能作为父类:结构体不能作为其他类或结构体的父类。
1.2-无法重写方法:结构体中的方法也不能被子类重写。

虽然结构体不能继承,但你可以通过实现协议来达到类似继承的效果,因为结构体也可以遵循协议。

2-关于改变属性的注意事项:
在结构体中,如果你需要在方法中修改结构体内的属性值,需要将该方法标记为 mutating。
因为结构体是值类型,方法默认是不能修改结构体内部属性的,除非显式声明为 mutating 方法。

struct MyStruct {
var property: Int

mutating func changeProperty(newValue: Int) {
self.property = newValue
}
}

var instance = MyStruct(property: 10)
instance.changeProperty(newValue: 20)
print(instance.property) // 输出:20

在这个例子中,changeProperty 方法需要修改 property 属性的值,
因此需要使用 mutating 关键字来标记这个方法。
这样就可以在结构体的实例上调用这个方法来改变属性的值。

2.6 类自动生成的初始化方法与结构体自动初始化方法有何区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在 Swift 中,类和结构体在自动生成初始化方法(称为默认初始化方法)方面有一些区别。
下面是它们的主要区别:

1-类(Class)自动生成的初始化方法:
1.1-默认初始化方法:对于类,如果没有提供任何指定初始化方法(Designated Initializer),
则会自动生成一个无参数的默认初始化方法(Default Initializer)。

1.2-指定初始化方法:如果类中包含了任何指定初始化方法,那么默认初始化方法将不会被自动生成。
指定初始化方法是一个能够完全初始化类中所有属性的初始化方法。

1.3-便捷初始化方法:便捷初始化方法(Convenience Initializer)是用来调用指定初始化方法的辅助初始化方法。

2-结构体(Struct)自动生成的初始化方法:

2.1-成员逐一初始化方法:对于结构体,如果没有提供任何自定义的初始化方法,
则会自动生成一个成员逐一初始化方法(Memberwise Initializer)。
该方法的参数列表包含了结构体中每个成员变量的参数,用于对结构体的每个成员变量进行初始化。

2.2-自定义初始化方法:你可以为结构体提供自定义的初始化方法,以满足特定的需求。
自定义初始化方法可以提供额外的参数,并在初始化过程中执行一些特定的逻辑操作。

总的来说,类和结构体在自动生成初始化方法方面的主要区别在于类具有默认初始化方法,
而结构体具有成员逐一初始化方法。这是因为类可以继承,因此默认初始化方法可以被子类继承和重写,
而结构体不支持继承,所以只有成员逐一初始化方法。

2.7 struct 与 class有什么区别?值类型和引用类型,继承,初始化方法,属性值改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在 Swift 中,struct(结构体)和 class(类)是两种不同的类型,它们有以下区别:

1. 值类型 vs 引用类型:
1.1-struct 是值类型:当你创建一个结构体的实例并将其分配给常量或变量时,实际上是将值复制到新的常量或变量中。
每个实例都有自己唯一的内存副本,因此对一个实例的更改不会影响其他实例。

1.2-class 是引用类型:当你创建一个类的实例并将其分配给常量或变量时,实际上是将引用(指针)分配给了常量或变量。
多个常量或变量可能引用相同的实例,因此对一个实例的更改会影响到其他引用了该实例的常量或变量。

2. 继承:
2.1-class 支持继承:类可以通过子类继承其属性和方法,并且可以添加额外的属性和方法或者重写现有的属性和方法。
2.2-struct 不支持继承:结构体不能被继承,因为它们是值类型而不是引用类型。

3. 初始化方法:
3.1-class 可以有多种初始化方法:类可以定义多个初始化方法,包括指定初始化方法和便捷初始化方法。
3.2-struct 自动提供成员逐一初始化方法:结构体会自动生成一个成员逐一初始化方法,
用于对结构体的成员变量进行初始化。如果需要,也可以自定义初始化方法。

4. 属性值改变:
4.1-class 的属性可以在实例被声明为常量时修改:即使将类的实例分配给常量,类的属性仍然可以通过该常量修改。
4.2-struct 的属性不能在实例被声明为常量时修改:如果将结构体的实例分配给常量,则无法修改结构体的属性。

总的来说,struct 和 class 在 Swift 中有着不同的用途和行为,你应该根据需要选择合适的类型。
如果需要传递值或者使用简单的数据结构,可以使用结构体。如果需要引用传递和继承特性,则应该使用类。

2.8 如何给结构体,类,枚举增加subscript下标?subscript可以用来做什么?

1
2
3
4
5
6
7
8
9
10
在 Swift 中,你可以通过在结构体、类和枚举中实现 subscript 下标来为它们增加下标功能。
下标允许你通过方括号语法访问集合、序列或其他容器中的元素,就像访问数组中的元素一样

下标可以用来做什么?

下标提供了一种简洁、直观的方式来访问集合、序列或其他容器中的元素。你可以使用下标来:

通过索引访问集合、数组等数据结构中的元素。
为自定义类型提供类似数组或字典的访问方式。
提供更简洁的语法来操作特定位置的值,例如矩阵、二维数组等

三 参考

  • 简书—Swift结构体,类,枚举