在Swift中将对象转换为不太特定的协议有什么风险吗?

krugob8w  于 2023-08-02  发布在  Swift
关注(0)|答案(2)|浏览(107)

在下面的例子中,让TextViewController符合UIScrollViewDelegate而不是UITextViewDelegate,然后将文本视图转换为滚动视图(或者声明为UIScrollView)是否合理?如果没有,你建议如何做?
我不想让代码看起来像TextViewController可以符合UITextViewDelegate,因为这会让人感到困惑。我假设如果一个非可选方法被添加到UITextViewDelegate中,强制转换将失败。此外,不允许使用private/fileprivate扩展,但这会减少我的担忧。

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()
    }
}

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

3phpmpom

3phpmpom1#

(添加一个答案,因为这太长,无法评论。)
为了补充一些关于“为什么”的更明确的细节,你正在做的事情是一个坏主意:
你在这里遇到的情况是一个Objective-C-ism,通常在Swift中是不允许的;具体地说,这里处理的是子类型(UITextView : UIScrollView)上的一个属性,该属性比其超类型的属性(UITextViewDelegate : UIScrollViewDelegate)更具体。这违反了Liskov substitution principle,但是Objective-C的类型系统是weak,足以允许这样做。
您已经隐式地调用了这一点,但对读者来说是简短的:UIScrollViewdelegate属性允许 * 任何 * UIScrollViewDelegate符合对象分配给它;毕竟,任何符合协议的东西都将满足UIScrollView对如何处理事件的信息的需要。然而,UITextView覆盖了这个属性,只接受符合更具体UITextViewDelegate协议的对象,因为它有更多的问题需要向委托人询问如何处理各种场景。在正常的子类型规则下(以及Liskov替换原则成立时),如果你取一个对象a: A,并将其强制转换为它的超类B,那么它的所有属性应该完全保持原样。除了对于UITextView不是这样。当你将UITextView的示例强制转换为UIScrollView时,它的delegate属性突然变得 * 更一般 *,这意味着你可以将一个UIScrollViewDelegate赋给delegate属性,而不是 * UITextViewDelegate(因为,再一次,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不允许用不同类型的属性覆盖属性,以防止这种情况,同样也防止方法参数和返回值的反转。

72qzrwbm

72qzrwbm2#

我不推荐这样做。它有点工作(虽然它抛出警告),但它比它取代的东西更令人困惑。TextViewController肯定仍然可以实现UITextViewDelegate方法,并且它们将被调用;它们只需要标记为@objc。随着代码的发展,这就要求出现一些细微的bug。作为其他人(或者你自己,因为你忘记了你这样做了),尝试添加UITextViewDelegate方法,它们可能会被调用,也可能不会被调用,这取决于它们的添加方式(在Swift或ObjC中,或者有或没有@objc)。你可以假设这永远不会发生,但是“这使得未来的bug更容易被创建”是一个风险。
不太清楚为什么要隐藏一致性,但是如果它很重要,有几种方法可以这样做。
最简单的方法是将textView本身设为UIScrollView:

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

...

_textView.delegate = self

字符串
(If您将textView设为私有,不需要UITextView访问权限,这可能会更简单。)
或者,您可以为UITextView创建一个内部私有委托,这样它就不会被公开。不清楚公开UITextViewDelegate比公开UIScrollViewDelegate更重要。但如果你想隐藏这些细节,那么我会把整个内容隐藏在一个内部类型中。
也就是说,你的问题是征求建议。建议是不要这样做。如果问题是“这会起作用吗”,答案是肯定的,除了它抛出了一个警告。

相关问题