什么时候需要对同一对象的两个引用?


20

特别是在Java中,但也可能在其他语言中:对同一对象有两个引用何时有用?

例:

Dog a = new Dog();
Dob b = a;

在这种情况下有用吗?a当您想与所代表的对象进行交互时,为什么这是首选的解决方案a


这是Flyweight模式背后的本质。

@MichaelT您能详细说明吗?
Bassinator 2014年

7
如果a并且b 始终引用相同Dog,则没有任何意义。如果他们有时会这样做,那么它很有用。
加布2014年

Answers:


45

例如,当您想在两个单独的列表中包含相同的对象时:

Dog myDog = new Dog();
List dogsWithRabies = new ArrayList();
List dogsThatCanPlayPiano = new ArrayList();

dogsWithRabies.add(myDog);
dogsThatCanPlayPiano.add(myDog);
// Now each List has a reference to the same dog

另一个用途是当同一对象扮演多个角色时

Person p = new Person("Bruce Wayne");
Person batman = p;
Person ceoOfWayneIndustries = p;

4
抱歉,但我相信对于您的布鲁斯·韦恩(Bruce Wayne)的例子来说,存在设计缺陷,蝙蝠侠和首席执行官职位应该是您的角色。
Silviu Burcea 2014年

44
-1用于揭示蝙蝠侠的秘密身份。
Ampt

2
@Silviu Burcea-我非常同意Bruce Wayne的例子不是一个好例子。一方面,如果全局文本编辑将名称“ ceoOfWayneIndustries”和“蝙蝠侠”更改为“ p”(假设名称没有冲突或范围没有变化),并且程序的语义发生了变化,那么它们的含义就被破坏了。引用的对象代表实际含义,而不是程序中的变量名称。为了具有不同的语义,它要么是一个不同的对象,要么被具有比引用更多的行为的引用所引用(应该透明),因此不是2个以上引用的示例。
gbulmer 2014年

2
尽管所写的布鲁斯·韦恩(Bruce Wayne)示例可能行不通,但我认为上述意图是正确的。也许更接近的示例是Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;-您具有多个可能的值(或可用值的列表)以及对当前活动/选定的值的引用。
Dan Puzey 2014年

1
@gbulmer:我想你不能引用两个对象;参考currentPersona指向一个对象或另一个,但不能同时指向两个对象。特别是,它很容易被可能currentPersona永远不会设置为bruce,在这种情况下,它肯定不是两个对象的引用。在我的示例中,我会说batmancurrentPersona都是对同一实例的引用,但是在程序中具有不同的语义。
Dan Puzey 2014年

16

这实际上是一个令人惊讶的深刻问题!现代C ++的经验(以及从现代C ++汲取的语言,例如Rust)表明,您通常不希望这样做!对于大多数的数据,你想有一个单一的独特的(“拥有”)的参考。这也是线性系统的主要原因之一。

但是,即使那样,您通常也希望使用一些短暂的“借用”引用来短暂访问内存,但不会在数据存在的大部分时间内持续使用。最常见的情况是将对象作为参数传递给其他函数(参数也是变量!):

void encounter(Dog a) {
  hissAt(a);
}

void hissAt(Dog b) {
  // ...
}

不常见的情况是,您根据条件使用两个对象之一,而实际上无论选择哪种对象都做相同的事情:

Dog a, b;
Dog goodBoy = whoseAGoodBoy ? a : b;
feed(goodBoy);
walk(goodBoy);
pet(goodBoy);

回到更常见的用途,但保留局部变量,我们转到字段:例如,GUI框架中的小部件通常具有父级小部件,因此,包含十个按钮的大框架将至少具有十个指向它的引用(以及更多其他内容)。从它的父母可能是从事件侦听器等)。任何种类的对象图和某些种类的对象树(带有父/兄弟引用的对象树)都有多个对象引用每个相同的对象。实际上,每个数据集实际上都是一个图形;-)


3
“少见的情况”很常见:将对象存储在列表或地图中,检索所需的对象,然后执行所需的操作。您不会从地图或列表中删除它们的引用。
SJuan76

1
@ SJuan76“较不常见的情况”仅涉及从局部变量中获取任何涉及数据结构的内容。

由于引用计数的内存管理,仅一个引用的理想选择吗?如果是这样,这将是值得一提的是,动机是不相关的不同的垃圾收集其他语言(如C#,使用Java,Python)
MarkJ

@MarkJ线性类型不仅是理想的类型,许多现有代码在不知不觉中也符合它,因为在很多情况下它在语义上都是正确的。它有潜在的性能优势(但没有引用计数,也不跟踪GCS,当您使用了不同的策略,实际上利用这些知识来省略这两个引用计数和跟踪它不仅有助于)。更有趣的是将其应用于简单的确定性资源管理。想想RAII和C ++ 11会移动语义,但是会更好(应用更多,错误会被编译器捕获)。

6

临时变量:考虑以下伪代码。

Object getMaximum(Collection objects) {
  Object max = null;
  for (Object candidate IN objects) {
    if ((max is null) OR (candidate > max)) {
      max = candidate;
    }
  }
  return max;
}

变量maxcandidate可能指向同一对象,但是变量分配使用不同的规则和在不同的时间更改。


3

为了补充其他答案,您可能还希望从同一位置开始以不同的方式遍历数据结构。例如,如果您有个BinaryTree a = new BinaryTree(...); BinaryTree b = a,则可以使用,遍历树的最左侧路径,使用遍历a其最右侧路径b

while (!a.equals(null) && !b.equals(null)) {
    a = a.left();
    b = b.right();
}

自从我写Java以来​​已经有一段时间了,所以代码可能不正确或不明智。把它当作伪代码。


3

当您有多个对象都可以回调到另一个可以无上下文使用的对象时,此方法非常有用。

例如,如果您具有选项卡式界面,则可能具有Tab1,Tab2和Tab3。您可能还希望能够使用公共变量,而不管用户位于哪个选项卡上,以简化代码并减少在运行过程中以及用户位于哪个选项卡上时一目了然的情况。

Tab Tab1 = new Tab();
Tab Tab2 = new Tab();
Tab Tab3 = new Tab();
Tab CurrentTab = new Tab();

然后,在每个编号的选项卡onClick中,您可以更改CurrentTab以引用该选项卡。

CurrentTab = Tab3;

现在,在您的代码中,您可以不受惩罚地调用“ CurrentTab”,而无需知道您实际在哪个选项卡上。您还可以更新CurrentTab的属性,它们将自动向下流动到引用的选项卡。


3

在很多情况下,b 必须引用未知的“ a”才能有用。特别是:

  • 任何时候您都不知道b编译时指向什么。
  • 任何时候需要遍历集合时,无论在编译时是否已知
  • 任何时候只要范围有限

例如:

参量

public void DoSomething(Thing &t) {
}

t 是对外部作用域中变量的引用。

返回值和其他条件值

Thing a = Thing.Get("a");
Thing b = Thing.Get("b");
Thing biggerThing = Thing.max(a, b);
Thing z = a.IsMoreZThan(b) ? a : b;

biggerThingz分别是a或的引用b。我们不知道在编译时哪个。

Lambda及其返回值

Thing t = someCollection.FirstOrDefault(x => x.whatever > 123);

x是一个参数(上面的示例1),并且t是一个返回值(上面的示例2)

馆藏

indexByName.add(t.name, t);
process(indexByName["some name"]);

index["some name"]在很大程度上是更精致的外观b。它是创建并填充到集合中的对象的别名。

循环

foreach (Thing t in things) {
 /* `t` is a reference to a thing in a collection */
}

t 是对由迭代器(上一示例)返回的项目(示例2)的引用。


您的示例很难理解。别误会我,我经历了他们,但是我必须努力。将来,我建议将您的代码示例分成多个单独的块(有一些注释专门说明每个块),而不是将一些不相关的示例放在一个块中。
Bassinator

1
@HCBPshenanigans我不希望您更改选定的答案;但是,我已经更新了我的软件,希望可以提高可读性并填写所选答案中缺少的一些用例。
svidgen 2014年

2

这很关键,但是恕我直言是值得理解的。

所有OO语言都始终创建引用的副本,并且永远不会“无形地”复制对象。如果OO语言以任何其他方式工作,编写程序将更加困难。例如,函数和方法永远无法更新对象。如果没有显着增加的复杂性,Java和大多数OO语言几乎是不可能使用的。

程序中的对象应该具有某些含义。例如,它代表了现实世界中特定的事物。通常,对同一事物有很多引用是有意义的。例如,我的家庭住址可以提供给许多人和组织,并且该住址总是指相同的地理位置。因此,第一点是,对象通常代表特定,真实或具体的事物;因此,能够对同一事物进行多次引用非常有用。否则将很难编写程序。

每次将您a作为参数/参数传递给另一个函数(例如,调用
foo(Dog aDoggy);
或应用方法)时a,基础程序代码都会复制该引用,以生成对同一对象的第二个引用。

此外,如果具有复制的引用的代码在不同的线程中,则可以同时使用这两者来访问同一对象。

因此,在大多数有用的程序中,将存在对同一对象的多个引用,因为这是大多数OO编程语言的语义。

现在,如果我们考虑一下,因为通过引用传递是许多OO语言(C ++支持这两种语言)中唯一可用的机制,因此我们可能希望它是“正确的” 默认行为。

恕我直言,使用引用是正确的default,原因有两个:

  1. 它保证了在两个不同位置使用的对象的值相同。想象一下将一个对象放入两个不同的数据结构(数组,列表等),并对更改它的对象执行一些操作。这可能是调试的噩梦。更重要的是,这两个数据结构中的同一对象,或者程序存在错误。
  2. 您可以愉快地将代码重构为多个函数,或将代码从多个函数合并为一个,并且语义不会改变。如果该语言不提供参考语义,则修改代码将更加复杂。

还有一个效率论证。复制整个对象的效率不及复制参考。但是,我认为这没有意义。对同一对象的多个引用更有意义,并且更易于使用,因为它们与现实世界的语义相匹配。

因此,恕我直言,通常有多个对同一个对象的引用是有意义的。在算法上下文中没有意义的异常情况下,大多数语言都提供了“克隆”或深拷贝的功能。但是,这不是默认值。

我认为有人认为这应该是默认值,他们使用的语言不提供自动垃圾收集功能。例如,老式的C ++。问题在于,他们需要找到一种方法来收集“死”对象,而不是回收仍然需要的对象。对同一个对象有多个引用会很困难。

我认为,如果C ++具有足够低廉的垃圾收集成本,以便所有引用的对象都被垃圾收集,那么大部分异议就会消失。仍然会有某些情况下是参考语义并不需要什么。但是,以我的经验,可以识别这些情况的人通常也能够选择适当的语义。

我相信有证据表明,C ++程序中有大量代码可以处理或减轻垃圾收集。但是,编写和维护这种“基础结构”代码会增加成本。它可以使语言更易于使用或更强大。因此,例如Go语言的设计重点是补救C ++的某些弱点,除了垃圾回收之外别无选择。

在Java上下文中,这当然是无关紧要的。它也被设计为易于使用,垃圾收集也是如此。因此,具有多个引用是默认的语义,并且在存在引用的情况下不回收对象的意义上来说是相对安全的。当然,它们可能会被数据结构所保留,因为该程序在真正完成一个对象时无法正确整理。

因此,回过头来回答您的问题(有点概括),您何时会希望对同一个对象有多个引用?我几乎可以想到的每种情况。它们是大多数语言参数传递机制的默认语义。我建议这是因为处理现实世界中存在的对象的默认语义必须通过引用来实现(因为“实际对象在那儿”)。

任何其他语义将更难处理。

Dog a = new Dog("rover");  // initialise with name 
DogList dl = new DogList()
dl.add(a)
...
a.setOwner("Mr Been")

我建议“漫游器” dl应该是受其影响setOwner或程序难以编写,理解,调试或修改的“漫游器” 。我认为大多数程序员否则会感到困惑或沮丧。

后来,狗被卖掉了:

soldDog = dl.lookupOwner("rover", "Mr Been")
soldDog.setOwner("Mr Mcgoo")

这种处理是常见且正常的。因此,引用语义是默认语义,因为它通常最有意义。

简介:对同一个对象有多个引用总是有意义的。


好点,但这更适合作为评论
Bassinator 2014年

@HCBPshenanigans-我认为这一点可能太简洁了,没有说太多。因此,我在推理上作了扩展。我认为这是对您问题的“元”答案。总结,对同一对象的多个引用对于使程序易于编写至关重要,因为程序中的许多对象代表现实世界中事物的特定或唯一实例。
gbulmer 2014年

1

当然,您可能会遇到的另一种情况是:

Dog a = new Dog();
Dog b = a;

是当您维护代码并b曾经是另一只狗或另一类,但现在由提供服务时a

通常,从中期来看,您应该重新编写所有代码以a直接引用,但这可能不会立即发生。


1

只要您的程序有机会将一个实体从多个位置拉入内存,就可能需要这样做,这可能是因为不同的组件正在使用它。

身份地图提供了实体的确定本地存储,因此我们可以避免使用两个或多个单独的表示形式。当我们两次表示同一个对象时,如果对象的一个​​引用在另一个实例之前保持其状态更改,则客户端将冒导致并发问题的风险。我们的想法是,我们要确保客户始终在处理对实体/对象的明确引用。


0

我在编写Sudoku求解器时使用了它。当我在处理行时知道一个单元格的编号时,我希望包含列在处理列时也知道该单元格的编号。因此,列和行都是重叠的Cell对象的数组。就像显示的已接受答案一样。


-2

在Web应用程序中,对象关系映射器可以使用延迟加载,以便对同一数据库对象的所有引用(至少在同一线程内)指向同一事物。

例如,如果您有两个表:

小狗:

  • id | owner_id | 名称
  • 1 | 1 | 树皮肯特

拥有者:

  • id | 名称
  • 1 | 我
  • 2 | 您

如果进行以下调用,那么您的ORM可以采取几种方法:

dog = Dog.find(1)  // fetch1
owner = Owner.find(1) // fetch2
superdog = owner.dogs.first() // fetch3
superdog.name = "Superdog"
superdog.save! // write1
owner = dog.owner // fetch4
owner.name = "Mark"
owner.save! // write2
dog.owner = Owner.find(2)
dog.save! // write3

在幼稚策略中,对模型和相关引用的所有调用都检索单独的对象。Dog.find()Owner.find()owner.dogs,并且dog.owner导致数据库打到第一次,然后将它们保存到内存中。所以:

  • 从至少4次获取数据库
  • dog.owner与superdog.owner不同(单独获取)
  • dog.name与superdog.name不同
  • dog和superdog都尝试写入同一行,并且会覆盖彼此的结果:write3将撤消write1中的名称更改。

如果没有参考,则将有更多的访存,使用更多的内存并引入了覆盖早期更新的可能性。

假设您的ORM知道对dogs表第1行的所有引用都应指向同一事物。然后:

  • fetch4可以消除,因为内存中存在一个与Owner.find(1)相对应的对象。fetch3仍将至少进行索引扫描,因为可能还有其他所有者拥有的狗,但不会触发行检索。
  • 狗和超级狗指向相同的对象。
  • dog.name和superdog.name指向同一对象。
  • dog.owner和superdog.owner指向同一对象。
  • write3不会覆盖write1中的更改​​。

换句话说,使用引用有助于将单个真理点(至少在该线程内)作为数据库中的行的原则进行整理。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.