DEV Community

Ting
Ting

Posted on

Swift 静默弃用警告

(中文找不到合适的表达,实际上想说的是 suppress/silence deprecation warning in Swift,压制弃用警告?)

问题

在开发 SDK 时,会遇到一种和弃用警告相关的问题:当我们把一个类型、属性、方法标为弃用后,如何使其在用户使用的时候产生警告,而在我们开发过程中忽略警告呢?

因为在开发过程中,我们始终追求无警告、无错误,所以不能因为自己添加了弃用警告而搬起石头砸自己的脚。在其他语言中,时常有一些宏来告诉编译器忽略这些警告,比如

#pragma clang diagnostic ignored
Enter fullscreen mode Exit fullscreen mode

然而,直到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? {
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

在标注这个方法为弃用后,如果我们调用这个方法:

let album = Album()
let image = album.getImage()
Enter fullscreen mode Exit fullscreen mode

方法弃用警告

编译器会⚠️警告该方法已弃用。这个警告对于用户来说是有用的——他们可以替换掉弃用的方法;对于我们自己而言,我们不想看到 SDK 里面用这个方法时产生警告,所以需要变通一下。

private protocol GetLatestImage {
    func getImage() -> UIImage?
}
extension Album: GetLatestImage {}
Enter fullscreen mode Exit fullscreen mode

添加这个协议以后,在我们自己开发 SDK 时如下调用这个方法:

let album = Album()
// let image = album.getImage()
let image = (album as any GetLatestImage).getImage()
Enter fullscreen mode Exit fullscreen mode

弃用警告就不会再出现了。

需要注意的是,在将 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)
}
Enter fullscreen mode Exit fullscreen mode

当 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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

枚举弃用警告

类似地,我们也可以用协议来绕过这个警告。

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")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

这样,在使用枚举值时,就不会产生警告了。

弃用属性

弃用属性的情况很类似弃用方法。但是因为属性本身要被初始化,所以弃用以后,会影响好几个地方。比如:

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
    }
}
Enter fullscreen mode Exit fullscreen mode

属性弃用警告

这种情况下,我们需要一些额外的处理,来保证之前被弃用的属性在最终被移除之前仍然可用,但是不会给出警告。话不多说,直接上代码:

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  // 无弃用警告
}
Enter fullscreen mode Exit fullscreen mode

通过添加一个隐藏的属性,我们便可以在保留原先被弃用的属性的语义的前提下,压制警告的产生。

弃用类型

前面三种情况都是只弃用某个类型中的一部分——方法、枚举值、属性。有时候,一整个类型都得被弃用。可以想像这样一种情况:写了一个程序兼容不同的汽车品牌,结果 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
    }
}
Enter fullscreen mode Exit fullscreen mode

类型弃用警告

这种情况下,问题和之前就不太一样了。因为类型可以被当作参数传来传去,也可以成为其他类型里属性的类型,所以所有用到弃用类型的地方都会产生警告。比如:

@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
    }
}
Enter fullscreen mode Exit fullscreen mode

类型弃用后会多处报警

此时,有两种选择:一是使用类似前面所说的方法,将该弃用类型里的所有属性、方法全部弃用,并用协议的变通方法压制警告信息;二是使用基础类型来搭建一个替代品,从而不再使用被弃用的类型。这里展示一下第二种思路:

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
    }
}
Enter fullscreen mode Exit fullscreen mode

这样,所有的需要静默弃用警告的情形,就都能解决了。

结语

Swift 6.1 引入了 SE-0443 来实现更精细化的静默警告信息的编译参数。然而,Swift 暂时还做不到按照代码块来压制警告。因此,2025年的当下,我们仍旧不得不用“奇技淫巧”来去除那些恼人的警告。

好在,在 SDK 层面压制警告,并不会影响到用户在需要时得到这些弃用警告的消息。

Top comments (0)