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)