(中文找不到合适的表达,实际上想说的是 suppress/silence deprecation warning in Swift,压制弃用警告?)
问题
在开发 SDK 时,会遇到一种和弃用警告相关的问题:当我们把一个类型、属性、方法标为弃用后,如何使其在用户使用的时候产生警告,而在我们开发过程中忽略警告呢?
因为在开发过程中,我们始终追求无警告、无错误,所以不能因为自己添加了弃用警告而搬起石头砸自己的脚。在其他语言中,时常有一些宏来告诉编译器忽略这些警告,比如
#pragma clang diagnostic ignored
然而,直到2025年,Swift 6.2 仍旧没有提供类似的工具。因此,Swift 开发者只能另辟蹊径。借 Quinn “The Eskimo!” 的发言:
Needless to say, the second point (必须用奇技淫巧绕过) does not make me happy (r. 31131633)-:
示例
以下我分别以几种弃用情况来举例:弃用方法、弃用枚举值、弃用属性、弃用类型。
弃用方法
这种情形相对来说是最简单直接的。假如我们类型里有一个方法需要弃用:
public class Album {
/// - Attention: Deprecated at 1.0.
@available(*, deprecated, message: "getImage() is deprecated and will be retired")
public func getImage() -> UIImage? {
...
}
}
在标注这个方法为弃用后,如果我们调用这个方法:
let album = Album()
let image = album.getImage()
编译器会⚠️警告该方法已弃用。这个警告对于用户来说是有用的——他们可以替换掉弃用的方法;对于我们自己而言,我们不想看到 SDK 里面用这个方法时产生警告,所以需要变通一下。
private protocol GetLatestImage {
func getImage() -> UIImage?
}
extension Album: GetLatestImage {}
添加这个协议以后,在我们自己开发 SDK 时如下调用这个方法:
let album = Album()
// let image = album.getImage()
let image = (album as any GetLatestImage).getImage()
弃用警告就不会再出现了。
需要注意的是,在将 album
实例转换为协议的类型时,需要添加 any
关键字。尽管目前版本的 Swift 不添加 any
不会强制报错,但自从引入了 some/any
关键字以后,Swift 对于如何使用泛型有了更加清晰严格的要求,所以为了未来不会报错,加上这个关键字比较安全。
我猜测,用协议来静默警告的原理,就是把编译时的弃用警告检测,延迟到了运行时。编译时无法确定运行时符合协议的实例的具体类型,自然就不会产生警告。可以看出,这种变通方法会浪费一些性能,也有点 code smell。但是在 Swift 没有原生的编译时解决方法的当下,也是不得已而为之。
弃用枚举值
假如我们有如下的 MyError
错误枚举类型:
public enum MyError: Error {
/// - Attention: Deprecated at 1.0.
@available(*, deprecated, message: "missingKey is deprecated and will be retired")
case missingKey(details: String)
}
当 switch-case 到这个枚举值时,便会产生警告:
extension MyError {
static func fromErrorCode(_ code: UInt) -> MyError {
switch code {
case 1:
return .missingKey(details: "The API key is missing.")
default:
fatalError("Unexpected error code")
}
}
}
类似地,我们也可以用协议来绕过这个警告。
extension MyError {
static func fromErrorCode(_ code: UInt) -> MyError {
// The sole purpose of this protocol is to silence a deprecation warning due to the enum case being deprecated.
protocol MyErrorProviding {
func missingKeyError(details: String) -> MyError
}
struct MyErrorProvider: MyErrorProviding {
@available(*, deprecated, message: "missingKey is deprecated and will be retired")
func missingKeyError(details: String) -> MyError {
return .missingKey(details: details)
}
}
switch code {
case 1:
// return .missingKey(details: "The API key is missing.")
return (MyErrorProvider() as any MyErrorProviding)
.missingKeyError(details: "The API key is missing.")
default:
fatalError("Unexpected error code")
}
}
}
这样,在使用枚举值时,就不会产生警告了。
弃用属性
弃用属性的情况很类似弃用方法。但是因为属性本身要被初始化,所以弃用以后,会影响好几个地方。比如:
public class Album {
/// - Attention: Deprecated at 1.0.
@available(*, deprecated, message: "image is deprecated and will be retired")
public let image: UIImage
init(image: UIImage) {
self.image = image
}
}
这种情况下,我们需要一些额外的处理,来保证之前被弃用的属性在最终被移除之前仍然可用,但是不会给出警告。话不多说,直接上代码:
public class Album {
/// - Attention: Deprecated at 1.0.
@available(*, deprecated, message: "image is deprecated and will be retired")
public var image: UIImage { self._image }
/// This is an internal property to use so we don't use the deprecated property.
let _image: UIImage
init(image: UIImage) {
self._image = image
}
}
private protocol GetLatestImage {
var image: UIImage { get }
}
extension Album: GetLatestImage {}
// 在调用的地方
.onAppear {
let image = UIImage(systemName: "photo")!
let album = Album(image: image)
// let albumImage = album.image // 有弃用警告
let albumImage = (album as any GetLatestImage).image // 无弃用警告
}
通过添加一个隐藏的属性,我们便可以在保留原先被弃用的属性的语义的前提下,压制警告的产生。
弃用类型
前面三种情况都是只弃用某个类型中的一部分——方法、枚举值、属性。有时候,一整个类型都得被弃用。可以想像这样一种情况:写了一个程序兼容不同的汽车品牌,结果 Apple Car 倒闭了,所有相关的方法和类型都要弃用 🥲:
// 👋🍎🚘
/// - Attention: Deprecated at 1.0.
@available(*, deprecated, message: "AppleCar is no longer relevant and will be retired")
public class AppleCar {
/// The unique identifier of the car.
let id: UUID
/// The model image of the car.
public let image: UIImage
init(image: UIImage) {
self.id = UUID()
self.image = image
}
}
这种情况下,问题和之前就不太一样了。因为类型可以被当作参数传来传去,也可以成为其他类型里属性的类型,所以所有用到弃用类型的地方都会产生警告。比如:
@available(*, deprecated, message: "AppleCar is no longer relevant and will be retired")
public class AppleCar {
// ...
}
public class XiaomiCar {
// ...
}
public class FutureGarage {
public var appleCars: [AppleCar] = []
public var xiaomiCars: [XiaomiCar] = []
init(appleCars: [AppleCar], xiaomiCars: [XiaomiCar]) {
self.appleCars = appleCars
self.xiaomiCars = xiaomiCars
}
}
此时,有两种选择:一是使用类似前面所说的方法,将该弃用类型里的所有属性、方法全部弃用,并用协议的变通方法压制警告信息;二是使用基础类型来搭建一个替代品,从而不再使用被弃用的类型。这里展示一下第二种思路:
public class FutureGarage {
@available(*, deprecated, message: "AppleCar is no longer relevant and will be retired")
public var appleCars: [AppleCar] {
rawAppleCars.map(AppleCar.init)
}
// 用构成被弃用类型的基础类型,来计算出弃用类型的值
// 如果复杂的话,可以新定义隐藏的内部类型作为这个用途
var rawAppleCars: [UIImage] = []
public var xiaomiCars: [XiaomiCar] = []
init(rawAppleCars: [UIImage], xiaomiCars: [XiaomiCar]) {
self.rawAppleCars = rawAppleCars
self.xiaomiCars = xiaomiCars
}
}
这样,所有的需要静默弃用警告的情形,就都能解决了。
结语
Swift 6.1 引入了 SE-0443 来实现更精细化的静默警告信息的编译参数。然而,Swift 暂时还做不到按照代码块来压制警告。因此,2025年的当下,我们仍旧不得不用“奇技淫巧”来去除那些恼人的警告。
好在,在 SDK 层面压制警告,并不会影响到用户在需要时得到这些弃用警告的消息。
Top comments (0)