在Swift中,为什么要使用委托和协议而不是直接传递实例?

18

我试图在Swift的视图之间传递变量,遇到了比较抽象的协议和代理概念。

然后我尝试在第二个视图中存储对第一个视图的引用并直接调用该视图上的函数。看起来这样做是可行的:

SCREEN 1

class Screen1: UIViewController {

    var myName = "Screen1"

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    //
    // checking if the segue to screen 2 is called and then passing a reference
    //
    override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
        if segue.identifier == "screen2Segue"{
            let vc = segue.destinationViewController as Screen2
            vc.storedReference = self
        }
    }

    func getName() -> String {
        return myName
    }
}

屏幕2

class Screen2: UIViewController {

    var storedReference:Screen1!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func testReference() {
        // calling a function on the stored reference to screen 1
        var str = storedReference.getName()
        println("Leaving screen 2, going to " + str)
    }
}

我的问题是:这段代码有什么问题?如果可以直接传递引用,为什么要使用代理和协议?

也许相关的是:视图何时被取消初始化并被完全替换为新的视图实例?我是否在旧实例上调用了“getName()”?


2
我不明白为什么这个问题被投票为主要基于观点的。协议和委托模式存在是有充分理由的,它们的工作方式并不是观点问题。 - vikingosegundo
我在想私有方法和属性。如果它们也存在于保护对象内部工作免受外界干扰的方式,那在某些情况下这是否是一种有效的方法?以上示例提到了Apple API中专门构建的代码(tableview),以允许第三方开发人员使用表视图。但在我的情况下,我只是构建一个简单的私有两页应用程序,将永远不会被任何人(包括我自己)使用或访问。 - Kokodoko
5个回答

14

协议对于分离实现和接口非常有用,这有助于增加代码的可重用性、可理解性和可测试性。

例如,假设您希望在某种列表中存储项目。一些可能的列表实现包括基于数组的实现和基于节点(链表)的实现。如果您声明一个名为List的协议,并拥有实现该协议的ArrayListLinkedList类,那么任何需要使用列表的内容(作为传递给方法的参数的变量,属性等)都可以将List作为变量类型使用,并能够在不关心列表是ArrayList还是LinkedList的情况下正常运行。您可以更改使用的类型或其实现方式,而无论正在使用它们的内容如何,因为只有在协议中声明的公开接口才会被看到。

协议也可以用于模拟类似多重继承的东西,因为一个类可以从超类继承,同时实现一个或多个接口。(例如,蝙蝠既是哺乳动物又是有翅膀的,因此可以将其表示为继承自实现Winged协议的Mammal类的Bat类)。

代理模式使用协议将一些职责委托给另一个对象,这对于代码分离和可重用性特别好。例如,在iOS中,UITableViewDelegate协议允许UITableView通过委托另一个对象处理事件来响应诸如单元格选择之类的内容。这已经被数千个应用程序中数百万个对象使用,而实现UITableViewUITableViewDelegate的苹果开发人员可能无需知道正在实现协议的对象的任何信息。

直接在视图控制器之间传递引用会导致第二个完全依赖于第一个。如果您希望更改应用程序流程以便从其他位置访问第二个视图控制器,则必须重写该视图控制器以使用新的起点。如果使用协议,不需要对第二个视图控制器进行任何更改。

如果您只有一个视图控制器怎么办?我正在编写一个应用程序扩展,它只有一个视图控制器和一些子UIViews。在那里使用协议真的很困难。例如:在根视图中,我有一个子视图,这个子视图有自定义手势、类型、一个类来操作更改视图,如果用户进行交互。通过使用协议,我必须通过多个级别传递委托-从根视图到子视图,从根视图到自定义手势、类型和一些类...我已经尝试使用全局变量,我认为这是一个更好的设计模式? - TomSawyer
1
@TomSawyer 设计模式的关键在于它们通常旨在使您的代码更易维护,但往往以短期内更加复杂的代价为代价。这意味着您需要选择适合您的应用程序的模式。对于像您这样只有一个视图控制器且可能只有您在工作的简单项目而言,我同意全局变量可能更适合您的用例。而如果您有数百个类和其他10个人在上面工作,则绝不希望全局变量漂浮。 - Kamaros
我最关心的是内存使用情况。你知道传递实例、使用协议和全局变量之间有什么不同吗? - TomSawyer
@TomSawyer,由于两种变量都使用引用,因此在内存使用方面没有区别。 - Woodstock
@Woodstock 错了。我已经测试过使用全局变量,这些变量将被缓存。如果您使用应用程序扩展,可以调用这些变量而不是重新声明它们。 - TomSawyer

9

设计原则中的一个基本原则是不要暴露比必须更多的设计细节。通过传递引用,您正在暴露整个对象。这意味着其他人可以调用其任何函数并访问其任何属性。并对它们进行更改,这是不好的。除了让其他人以可能不符合其既定目的的方式使用对象之外,如果您尝试更改对象,并发现它破坏了他人使用您不打算的某些内容,则还会遇到问题。因此,始终不暴露任何不必要的内容是一个好主意。这就是代理和协议的目的。它使对象完全控制暴露什么。更加安全,更好的设计。


1
但是如果您可以将方法和属性设置为私有或内部,那么您也保护了它们不被访问吗? - Kokodoko
1
是的,你是。协议和代理可以扩展它。如果您的类要提供三种类型的服务,每种服务只打算由一种其他类型使用,您可以为每种服务声明一个协议。其他类可以声明自己只是想使用适当的代表。因为您只公开API,所以可以自由地对类进行内部更改,而不可能破坏使用您的类的其他人。 - hcanfly

4

我觉得你没有完全理解什么是协议。

我总是说协议就像合同。
实现某些协议的委托对象承诺,它能够做一些代理人无法做到的事情。

在现实世界中,我的房子的管道出了问题。
我(代理人)叫来一个水管工(代表),让他修理。水管工(按照合约)承诺可以做到。这个承诺就是协议。只要他能修好,我就不关心他是如何做到的。

但这些合同不仅对代理很有用。
我正在编写一个食品订购应用程序。由于它有一个菜单,需要显示其中的项目。
我可以使用基本的继承,编写一个MenuItem类,所有子类都必须继承该类。
或者我写一个协议来表达:“无论你是什么对象,只要你满足这个合约,我们就有交易”。这使我可以创建许多不同的类或将现有类注释为类别,尽管我没有多重继承的工具。

实际上我两种方式都使用:我编写了一个协议MenuItem和一个符合该协议的类MenuItem。现在我可以使用简单的继承或使用不继承MenuItem类的类。

代码是Objective-C(抱歉:我还在向Swift转换)。

@protocol MenuItem <NSObject>

-(NSString *)name;
-(double) price;
-(UIColor *)itemColor;

@end


@interface MenuItem : NSObject <MenuItem>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@property (nonatomic, strong) UIColor *itemColor;

@end

#import "MenuItem.h"

@implementation MenuItem

-(id)initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    if (self) {
        self.name = [decoder decodeObjectForKey:@"name"];
        self.price = [decoder decodeDoubleForKey:@"price"];
        self.itemColor = [decoder decodeObjectForKey:@"itemColor"];
    }
    return self;
}

-(void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeDouble:self.price forKey:@"price"];
    [encoder encodeObject:self.name forKey:@"name"];
    [encoder encodeObject:self.itemColor forKey:@"itemColor"];
}


@end

苹果在NSObject方面使用了相同的架构:有一个协议和一个类NSObject。这使得那些没有完全继承自类NSObject的类可以充当NSObject。其中一个著名的例子是:NSProxy


在您的情况下,Screen1承诺能够理解由详细视图控制器Screen2发送的消息。这样可以实现解耦:任何理解Screen1协议的对象都可以使用。此外,它有助于维护健康的对象树,因为我们不必进行循环导入。但是一般来说,您必须记住,委托者(Screen2)必须保持对其委托的弱引用,否则我们就会有一个保留循环。


当然,UITableView是一个重要的例子:
表视图对象了解有关渲染其单元格、处理滚动等的所有信息。但是编写它的工程师无法知道您想让您的表视图看起来像什么。这就是为什么他引入了一个代理来给你创建正确的单元格的机会。由于他也不知道您的数据长什么样,所以他还引入了数据源- 它的工作方式与代理完全相同:您将被要求提供有关所需数据的所有信息。


2
这主要是一个意见问题,所以这个问题可能应该被关闭,但我认为开发者社区整体上对此达成了共识,因此我还是会回答它。 软件架构(代码结构设计)中的一个重要概念是关注点分离。基本原则是将代码需要完成的任务分解成小组件,每个组件只有一个明确定义的目的。每个组件都应该能够独立地运行,除了直接与之交互的组件外,不需要过多考虑其他组件。
这有助于大大提高代码的可重用性。如果您设计了一个独立于大多数/如果不是所有其他组件的小组件,您可以轻松地将其插入到代码或其他应用程序的其他部分中。以UITableView为例。通过使用委托模式,每个开发人员都可以轻松创建一个表视图并用任何他们想要的数据填充它。由于该数据源是一个单独的对象(具有独立的关注点来生成数据),因此您可以将同一数据源附加到多个表视图上。想象一下iOS上的联系人列表。您将希望以多种方式访问相同的数据。而不是总是重写加载特定数据并以特定方式显示它的表视图,您可以根据需要多次重复使用数据源与不同的表视图。

这也有助于提高您的代码的可理解性。对于开发人员来说,要牢记应用程序状态的太多细节是很困难的。如果您的每个代码组件都被分解成小的、明确定义的责任,开发人员可以单独理解每个组件。他们还可以查看组件,并在不必查看特定实现的情况下做出准确的假设。对于小型应用程序来说,这并不是什么大问题,但随着代码库的增长,这变得非常重要。

通过传递对第一个视图控制器的引用,您使第二个视图控制器完全依赖于第一个。您无法在另一个实例中重用第二个视图控制器,其工作变得不太清晰。
分离关注点还有许多其他好处,但我认为这是两个令人信服和重要的好处。

如果第二个视图控制器不需要在另一个实例中重复使用怎么办?你谈论的是软件架构的一般性问题,我认为我们不应该拿UITableView作为例子。在某些特定的小项目中,比如扩展应用程序或某些只有1个UIViewController的应用程序中,如果你应用这种技术,它会使你的代码处理变慢,并且需要更长的时间来部署和开发。 - TomSawyer

0

我认为后者的问题在于单个类的多次重复利用。

以自定义UITableViewCell CustomTableViewCell 为例。假设你有两个具有tableView的Class A 和 Class B 都想使用CustomTableViewCell作为它们的cell,那么你现在有两个选择。你会更愿意:

A.为CustomTableViewCell使用一个名为CustomTableViewCellDelegate的代理/协议。在类CustomTableViewCell内声明一个名为“delegate”的单个对象,该对象实现了上述协议,并调用该对象(无论它调用哪个类)。

B.在CustomTableViewCell内为每个类(Class A, Class B)声明一个对象,以便您可以持有对它们的引用。

如果您需要将CustomTableViewCell用于多个类,则我认为您知道应该采取哪种选项。在CustomTableViewCell中为不同类声明多个对象在软件架构方面会很麻烦。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接