AnyObject, Any и any: когда что использовать?

AnyObject и Any получили новую опцию any, представленную в SE-355, что усложняет нам, разработчикам, понимание различий. Каждый вариант имеет свои варианты использования и подводные камни относительно того, когда их не следует использовать.

Any и AnyObject являются специальными типами в Swift, используемыми для стирания типов, и не имеют прямого отношения any. Помните о прописной букве A в этой статье, так как я расскажу как Any, так и any, которые имеют разные объяснения. Давайте углубимся в детали!

Когда использовать AnyObject?

AnyObject — это протокол, которому неявно соответствуют все классы. На самом деле стандартная библиотека содержит псевдоним типа AnyClass, представляющий AnyObject.Type.

print(AnyObject.self) // Prints: AnyObject
print(AnyClass.self) // Prints: AnyObject.Type

Все классы, типы классов или протоколы только для классов могут использовать AnyObject в качестве своего конкретного типа. Чтобы продемонстрировать, вы можете создать массив разных типов:

let imageView = UIImageView(image: nil)
let viewController = UIViewController(nibName: nil, bundle: nil)

let mixedArray: [AnyObject] = [
    // We can add both `UIImageView` and `UIViewController` to the same array
    // since they both cast to `AnyObject`.
    imageView,
    viewController,

    // The `UIViewController` type conforms implicitly to `AnyObject` and can be added as well.
    UIViewController.self
]

Только классы соответствуют AnyObject, что означает, что вы можете использовать его для ограничения реализации протоколов только ссылочными типами:

protocol MyProtocol: AnyObject { }

Вы можете использовать AnyObject, если вам нужна гибкость нетипизированного объекта. Я мог бы привести примеры использования коллекции любых объектов, которые при использовании можно привести к конкретному типу, но вместо этого я хотел бы предложить что-то другое.

Я не помню, чтобы я использовал AnyObject ни в одном из своих проектов, так как вместо этого я всегда мог использовать конкретные типы. Это также приводит к более читаемому коду, который легче понять другим разработчикам. Чтобы продемонстрировать вам это, я создал этот пример метода, который настраивает образ в целевой контейнер:

func configureImage(_ image: UIImage, in imageDestinations: [AnyObject]) {
    for imageDestination in imageDestinations {
        switch imageDestination {
        case let button as UIButton:
            button.setImage(image, for: .normal)
        case let imageView as UIImageView:
            imageView.image = image
        default:
            print("Unsupported image destination")
            break
        }
    }
}

Используя AnyObject в качестве пункта назначения, нам всегда нужно выполнять приведение и учитывать ошибки приведения, используя реализацию по умолчанию. Я бы всегда предпочел переписать это с использованием конкретных протоколов:

// Create a protocol to act as an image destination.
protocol ImageContainer {
    func configureImage(_ image: UIImage)
}

// Make both `UIButton` and `UIImageView` conform to the protocol.
extension UIButton: ImageContainer {
    func configureImage(_ image: UIImage) {
        setImage(image, for: .normal)
    }
}

extension UIImageView: ImageContainer {
    func configureImage(_ image: UIImage) {
        self.image = image
    }
}

// Create a new method using the protocol as a destination.
func configureImage(_ image: UIImage, into destinations: [ImageContainer]) {
    for destination in destinations {
        destination.configureImage(image)
    }
}

Полученный код стал чище, читабельнее и больше не требует обработки неподдерживаемых контейнеров. Экземпляры должны соответствовать протоколу ImageContainer, чтобы иметь возможность получать настроенный образ.

Когда использовать Any?

Any может представлять экземпляр любого типа, включая типы функций:

let arrayOfAny: [Any] = [
    0,
    "string",
    { (message: String) -> Void in print(message) }
]

Для меня к Any применяются те же правила, что и к AnyObject, а это означает, что вместо этого вы всегда должны стремиться использовать конкретные типы. Any является более гибким, позволяя вам приводить экземпляры любого типа, что затрудняет прогнозирование кода по сравнению с использованием конкретных типов.

Когда использовать any?

any введен в SE-335 и похож на Any и AnyObject, но имеет другое назначение, поскольку вы используете его для обозначения использования экзистенциального объекта. В следующем примере кода демонстрируется конфигуратор изображений с использованием примера кода из предыдущих примеров в этой статье:

struct ImageConfigurator {
    var imageContainer: any ImageContainer

    func configureImage(using url: URL) {
        // Note: This is not the way to efficiently download images
        // and is just used as a quick example.
        let image = UIImage(data: try! Data(contentsOf: url))!
        imageContainer.configureImage(image)
    }
}

let iconImageView = UIImageView()
var configurator = ImageConfigurator(imageContainer: iconImageView)
configurator.configureImage(using: URL(string: "https://picsum.photos/200/300")!)
let image = iconImageView.image

Как видите, мы указали на использование экзистенциального imageContainer, пометив наше свойство imageContainer ключевым словом any. Пометка протокола с использованием any будет применяться, начиная с Swift 6, поскольку это будет указывать на влияние такого использования протокола на производительность.

Экзистенциальные типы имеют значительные ограничения и влияние на производительность, а также обходятся дороже, чем использование конкретных типов, поскольку их можно изменять динамически. Следующий код является примером такого изменения:

let button = UIButton()
configurator.imageContainer = button

Наше свойство imageContainer может представлять любое значение, соответствующее нашему протоколу ImageContainer, и позволяет нам изменить его с представления изображения на кнопку. Для этого требуется динамическая память, что лишает компилятор возможности оптимизировать этот фрагмент кода. Вплоть до введения ключевого слова any разработчикам не было явных указаний, указывающих на эту стоимость производительности.

Уходить от any

В некотором смысле можно утверждать, что у any, Any и AnyObject есть что-то общее: используйте их с осторожностью.

Вы можете переписать приведенный выше пример кода, используя дженерики и избавившись от необходимости в динамической памяти:

struct ImageConfigurator<Destination: ImageContainer> {
    var imageContainer: Destination
}

Понимание последствий для производительности и знание того, как вместо этого переписать код, является важным навыком, которым должен владеть разработчик Swift.

Заключение

Any, any и AnyObject выглядят одинаково, но имеют важные отличия. На мой взгляд, лучше переписать свой код и убрать необходимость использовать любое из этих ключевых слов. Это часто приводит к более читаемому и предсказуемому коду.