在Swift中将对象转换为一个不太具体的协议是否存在任何风险?

huangapple go评论65阅读模式
英文:

Any risks to casting an object in Swift as a less specific protocol?

问题

在下面的示例中,将TextViewController遵循UIScrollViewDelegate而不是UITextViewDelegate,然后将文本视图转换为滚动视图(或者声明为UIScrollView)是否合理?如果不合理,您会如何建议处理这个问题?

我不想让代码看起来好像TextViewController可以遵循UITextViewDelegate,因为那会让人感到困惑。我假设如果将一个非可选方法添加到UITextViewDelegate,那么强制转换将失败。此外,不允许私有/文件私有扩展,但这将减少我的担忧。


class TextViewController: UIViewController {

    let textView = UITextView()

    override func viewDidLoad() {
        textView.delegate = self // 错误
        (textView as? UIScrollView)?.delegate = self // 可行
    }
}

extension TextViewController: UIScrollViewDelegate
{

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        在滚动时执行某些操作()
    }
}
extension UIScrollViewDelegate where Self: UIViewController {

    func 在滚动时执行某些操作() {
        // 执行某些操作。
    }
}

在这个示例中,我没有创建自己的协议,以使代码更加简洁。

英文:

In the example below, is it reasonable to make TextViewController conform to UIScrollViewDelegate instead of UITextViewDelegate and then cast the text view as a scroll view (or maybe declare it as a UIScrollView)? If not how would you suggest doing this?

I don't want to make the code look like TextViewController could conform to UITextViewDelegate because that would be confusing. I assume that the cast would fail if a non-optional method were added to UITextViewDelegate. Also, private/fileprivate extensions are not allowed, but that would reduce my concern.


class TextViewController: UIViewController {

    let textView = UITextView()

    override func viewDidLoad() {
        textView.delegate = self // Error
        (textView as? UIScrollView)?.delegate = self // OK
    }
}

extension TextViewController: UIScrollViewDelegate
{

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        doSomethingOnScroll()
    }
}
extension UIScrollViewDelegate where Self: UIViewController {

    func doSomethingOnScroll() {
        // Do something.
    }
}

I did not create my own protocol in this example to make the code more concise.

答案1

得分: 1

要为这个回答添加一些更明确的细节,以解释为什么你正在做的事情是一个不好的主意:

你在这里遇到的情况是 Objective-C 特有的,通常在 Swift 中是不允许的;具体来说,你在处理的是一个子类型(UITextView: UIScrollView)上的属性,它比其超类型的属性(UITextViewDelegate: UIScrollViewDelegate)更具体。这违反了里氏替换原则,但 Objective-C 的类型系统足够弱,可以容忍这种情况。

你已经隐含地提到了这一点,但简要解释一下:UIScrollViewdelegate 属性允许将任何符合 UIScrollViewDelegate 协议的对象分配给它;毕竟,符合该协议的任何对象都能满足 UIScrollView 处理事件的需求。然而,UITextView 重写了这个属性,只接受符合更具体的 UITextViewDelegate 协议的对象,因为它有更多关于如何处理各种情况的问题需要询问委托。根据正常的子类型化规则(并且当里氏替换原则成立时),如果你将一个对象 a: A 强制转换为其超类 B,则其所有属性应保持与原样... 但对于 UITextView 来说并非如此。当你将 UITextView 的实例转换为 UIScrollView 时,它的 delegate 属性突然变得更加通用,这意味着你可以分配一个不是 UITextViewDelegateUIScrollViewDelegatedelegate 属性(因为 UIScrollView 只关心 UIScrollViewDelegate 能告诉它的事情)。如果 UITextViewDelegate 包含了必需的方法,那么这将立即成为问题,因为在运行时,文本视图将尝试调用委托上不存在的方法。

你提到:

我假设如果向 UITextViewDelegate 添加了非可选方法,强制转换将失败

但需要强调的是这是不正确的UITextView 继承自 UIScrollView,因此 textView as? UIScrollView无条件成功 — 它只是保持了在访问超类型的属性时的 Objective-C 行为,就好像它们是静态类型属性一样。这意味着 (textView as? UIScrollView)?.delegate 将始终允许你将 UIScrollViewDelegate 对象分配给该属性,即使在运行时,UITextView 将调用它上面不存在的方法;虽然这不太可能发生(因为现有的委托对象无法隐式满足要求),但如果 UITextViewDelegate 确实包含非可选的方法要求,你将在运行时崩溃。

在你非常特定的情况下,这可能有效,但在一般情况下,这是非常不安全的做法。

值得一提的是,如果你尝试在纯 Swift 中重新创建这种情况,编译器不会允许这样做,这是有道理的:

protocol SuperDelegate {}
protocol SubDelegate: SuperDelegate {}

class SuperClass {
    var delegate: SuperDelegate?
}

class SubClass: SuperClass {
    override var delegate: SubDelegate?
    // ^ error: property 'delegate' with type '(any SubDelegate)?' cannot override a property with type '(any SuperDelegate)?'
}

Swift 不允许覆盖具有不同类型的属性,以防止这种情况发生,并同样防止方法参数和返回值的倒置。

英文:

(Adding an answer because this is too long for comments.)

To add some more explicit detail about why what you're doing is a bad idea:

The situation you're encountering here is an Objective-C-ism that normally isn't allowed in Swift; specifically, you're dealing here with a property on a subtype (UITextView : UIScrollView) that is more specific than its supertype's property (UITextViewDelegate : UIScrollViewDelegate). This violates the Liskov substitution principle, but Objective-C's type system is weak enough to happily allow this.

You already implicitly call this out, but briefly for readers: UIScrollView's delegate property allows any UIScrollViewDelegate-conforming object to be assigned to it; after all, anything that conforms to the protocol will satisfy UIScrollView's need for information about how to handle events. UITextView, however, overrides this property, accepting only objects which conform to the more specific UITextViewDelegate protocol, since it has more questions to ask the delegate about how to handle various scenarios. Under normal subtyping rules (and when the Liskov substitution principle holds), if you take an object a: A, and cast it to its superclass B, all of its properties should hold exactly as they were... except this is not true for UITextView. When you cast an instance of UITextView to UIScrollView, its delegate property suddenly becomes more general, meaning that you can assign a UIScrollViewDelegate which is not a UITextViewDelegate to the delegate property (since, again, UIScrollView only cares about things that UIScrollViewDelegate can tell it). If UITextViewDelegate had required methods, this would be immediately problematic, because at runtime, the text view would attempt to call a method on the delegate that it doesn't know how to answer.

You mention that

> I assume that the cast would fail if a non-optional method were added to UITextViewDelegate

but it's important to stress that this is not true. UITextView inherits from UIScrollView, and so textView as? UIScrollView will unconditionally succeed — it simply maintains Objective-C's behavior when accessing the properties on the supertype, as if they were statically-typed properties. What this means is that (textView as? UIScrollView)?.delegate will always allow you to assign a UIScrollViewDelegate object to the property, even if at runtime, UITextView will call methods on it that don't exist; although this can't reasonably happen (because existing delegate objects couldn't implicitly satisfy the requirement), if UITextViewDelegate did get non-optional method requirements, you would simply crash at runtime.

In your very specific case, this might work, but this is very unsafe to do in the general case.


For what it's worth, if you try to recreate this scenario in pure Swift, the compiler won't let you, for good reason:

protocol SuperDelegate {}
protocol SubDelegate: SuperDelegate {}

class SuperClass {
    var delegate: SuperDelegate?
}

class SubClass: SuperClass {
    override var delegate: SubDelegate?
    // ^ error: property 'delegate' with type '(any SubDelegate)?' cannot override a property with type '(any SuperDelegate)?'
}

Swift doesn't allow overriding properties with ones of different types at all to prevent this scenario, and similarly prevents inversions of method parameters and return values.

答案2

得分: 0

我不建议这样做。它似乎能够工作(尽管会引发警告),但比替代方案更令人困惑。TextViewController 绝对可以实现 UITextViewDelegate 方法,它们将被调用;只需将它们标记为 @objc。随着代码的演变,这会引发潜在的错误。当其他人(或者您自己,因为您可能忘记了自己这样做)尝试添加 UITextViewDelegate 方法时,根据它们是如何添加的(在 Swift 中还是在 ObjC 中,带或不带 @objc),它们可能会被调用或不会被调用。您可以假设这永远不会发生,但这会增加未来产生错误的风险。

目前还不太清楚您试图隐藏符合协议的原因,但如果这很重要,有几种方法可以实现。

最直接的方法是将 textView 本身设为 UIScrollView:

let _textView: UIScrollView = UITextView()
var textView: UITextView { _textView as! UITextView }

...

_textView.delegate = self

(如果将 textView 设为私有并且不需要 UITextView 访问,这甚至可以更简单。)

另一种方法是为 UITextView 创建一个内部私有的代理,以便不公开它。目前还不清楚暴露 UITextViewDelegate 比暴露 UIScrollViewDelegate 更重要。但如果您想要隐藏这些细节,那么我建议将整个内容都隐藏在一个内部类型中。

不过,话说回来,您的问题是寻求建议。建议是不要这样做。如果问题是“这样会起作用吗”,答案是是,只是会引发警告。

英文:

I do not recommend this. It kind of works (though it throws a warning), but it's more confusing than what it replaces. TextViewController can definitely still implement UITextViewDelegate methods and they'll be called; they just have to be marked @objc. That's asking for subtle bugs as the code evolves. As others (or yourself, as you forgot that you did this), try to add UITextViewDelegate methods, they may or may not be called depending on how their added (in Swift or ObjC, or with or without the @objc). You can assume that'll never happen, but "this makes future bugs easier to create" is the risk.

It's not quite clear why you're trying to hide the conformance, but there are several ways to do so if it's important.

The most straightforward is to make textView itself a UIScrollView:

let _textView: UIScrollView = UITextView()
var textView: UITextView { _textView as! UITextView }

...

_textView.delegate = self

(If you made textView private and don't need UITextView access, this could be even simpler.)

Alternately, you can could create an internal private delegate for the UITextView so it's not exposed. It's not clear how exposing UITextViewDelegate matters more than exposing UIScrollViewDelegate. But if you want to hide these details, then I'd hide the whole thing in an internal type.

That said, your question was for advice. The advice is not to do this. If the question is "will this work," the answer is yes, except it throws a warning.

huangapple
  • 本文由 发表于 2023年7月24日 00:55:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76749384.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定