如果可以使用后者,那么“父项x = new Child();”而不是“ Child x = new Child();”是一种不好的做法吗?


32

例如,我看过一些代码创建了这样的片段:

Fragment myFragment=new MyFragment();

它声明一个变量为Fragment而不是MyFragment,MyFragment是Fragment的子类。我对这行代码不满意,因为我认为该代码应为:

MyFragment myFragment=new MyFragment();

哪个更具体,是真的吗?

还是概括地说,使用以下做法是错误的做法:

Parent x=new Child();

代替

Child x=new Child();

是否可以在没有编译错误的情况下将前一个更改为后一个?


6
如果我们做后者,那么就没有必要再进行抽象/泛化了。当然也有例外...
Walfrat

2
在我正在研究的项目中,我的函数需要父类型,并运行父类型方法。我可以接受多个子代中的任何一个,这些(继承和重写)方法背后的代码是不同的,但是无论我使用哪个子代,我都希望运行该代码。
Stephen S

4
@Walfrat确实,当您实例化具体类型时,抽象没有什么意义,但是您仍然可以返回抽象类型,以使客户端代码变得无知。
Jacob Raihle

4
不要使用父母/孩子。使用接口/类(对于Java)。
托尔比约恩Ravn的安德森

4
Google对界面进行编程,以获取有关使用为什么Parent x = new Child()是一个好主意的大量解释。
凯文·沃克曼

Answers:


69

这取决于上下文,但是我认为您应该声明尽可能抽象的类型。这样,您的代码将尽可能通用,而不依赖无关的细节。

一个例子是有一个LinkedListArrayList都来自List。如果代码在任何类型的列表中都能很好地工作,则没有理由将其任意地限制在一个子类中。


24
究竟。举个例子,考虑一下List list = new ArrayList(),使用列表的代码并不关心列表是哪种类型,仅符合List接口的约定。将来,我们可以轻松地更改为其他List实现,并且根本不需要更改或更新基础代码。
Maybe_Factor '17

19
很好的答案,但是应该扩展大多数抽象类型,这意味着范围内的代码不应强制返回给子级以执行某些特定于子级的操作。
Mike

10
@Mike:我希望不用多说,否则所有变量,参数和字段都将声明为Object
JacquesB

3
@JacquesB:由于这个问题和答案引起了人们的广泛关注,我一直认为露骨明确对所有不同技能水平的人都是有用的。
Mike

1
它取决于上下文不是答案。
Billal Begueradj

11

JacquesB的回答是正确的,尽管有点抽象。让我更清楚地强调一些方面:

在代码段中,您明确地使用了new关键字,也许您想继续执行可能针对具体类型的进一步初始化步骤:然后,您需要使用该具体类型声明变量。

当您从其他地方获得该商品时,情况就不同了IFragment fragment = SomeFactory.GiveMeFragments(...);。在这里,工厂通常应返回最抽象的类型,而向下的代码不应依赖于实现细节。


18
“虽然有点抽象”。无礼。
rosuav

1
这难道不是一个编辑吗?
beppe9000 '17

6

可能存在性能差异。

如果派生类是密封的,则编译器可以内联并优化或消除虚拟调用。因此,可能会有明显的性能差异。

示例:ToString是一个虚拟呼叫,但String被密封。上StringToString是一个空操作,所以如果你声明为object这是一个虚拟的电话,如果声明一个String编译器知道没有派生类覆盖的方法,因为类是密封的,所以这是一个无操作。类似的考虑适用于ArrayListVS LinkedList

因此,如果您知道对象的具体类型(并且没有隐藏它的封装原因),则应声明为该类型。由于您刚刚创建了对象,因此知道具体类型。


4
在大多数情况下,性能差异很小。在大多数情况下(我已经听说过,这大约是90%的时间),我们应该忘记那些小的差异。只有当我们知道(最好通过测量)成为代码性能关键部分时,我们才应该考虑这种优化。
Jules

而且编译器知道即使将new new()分配给Parent,它也将返回Child,并且只要知道,它就可以使用该知识并调用Child()代码。
gnasher729

+1。在我的回答中也偷了你的榜样。= P
纳特

1
请注意,有一些性能优化可以使整个过程变得毫无意义。例如,HotSpot将注意到传递给函数的参数始终属于一个特定的类,并进行相应的优化。如果该假设被证明是错误的,则对该方法进行优化。但这在很大程度上取决于编译器/运行时环境。我最后一次检查的CLR甚至无法做到从注意到,p的相当琐碎的优化Parent p = new Child()始终是一个孩子..
VOO

4

关键区别在于所需的访问级别。关于抽象的每个好的答案都涉及动物,或者有人告诉我。

让我们假设您有一些动物- Cat BirdDog。现在,这些动物有几个常见的行为- move()eat()speak()。他们的饮食不同,说话也不同,但是如果我需要我的动物吃饭或说话,我并不在乎他们如何做。

但是有时候我会。我从来没有用过护卫猫或护卫鸟,但我确实有护卫犬。因此,当有人闯入我的房子时,我不能仅仅依靠说话来吓跑入侵者。我真的需要我的狗吠叫-这和他的说话不同,那只是一声。

所以在需要入侵者的代码中,我真的需要做 Dog fido = getDog(); fido.barkMenancingly();

但是大多数时候,我可以很高兴地让任何动物在招呼,喵喵叫或鸣叫以求佳节时招人喜欢。 Animal pet = getAnimal(); pet.speak();

因此,更具体地说,ArrayList有一些List没有的方法。值得注意的是trimToSize()。现在,在99%的情况下,我们对此并不关心。的List是几乎没有大的足以让我们不在乎。但是有时候是这样。因此,有时候,我们必须专门要求an ArrayList执行trimToSize()并使其后备数组变小。

请注意,这不是必须在构造时进行的-如果我们绝对确定,则可以通过return方法甚至是强制转换来完成。


困惑为什么这可能会引起反对。就个人而言,这似乎是规范的。除了可编程看门狗的想法...
corsiKa

为什么必须阅读页面才能获得最佳答案?评级糟透了。
Basilevs

感谢您的客气话。评级系统具有优缺点,其中之一是,以后的答案有时可能会被遗忘。它发生了!
corsiKa

2

一般来说,您应该使用

var x = new Child(); // C#

要么

auto x = new Child(); // C++

对于局部变量,除非有充分的理由使变量的类型与初始化时的类型不同。

(在这里,我忽略了以下事实:C ++代码很可能应该使用智能指针。在某些情况下,我实际上不知道或不超过一行。)

这里的总体思想是使用支持变量的自动类型检测的语言,使用它可以使代码更易于阅读并且可以执行正确的操作(也就是说,编译器的类型系统可以尽可能地优化并将初始化更改为新的如果使用了大多数抽象,则类型也可以使用)。


1
在C ++中,变量大多没有新的关键字创建:stackoverflow.com/questions/6500313/...
玛丽安Spanik

1
问题不关乎C#/ C ++,而是关乎至少具有某种形式的静态类型的语言中的一般面向对象的问题(即,用Ruby之类的东西来问是毫无意义的)。此外,即使使用这两种语言,我也没有真正理解为什么我们应该像您所说的那样做。
AnoE

1

很好,因为它易于理解并且易于修改此代码:

var x = new Child(); 
x.DoSomething();

很好,因为它传达了意图:

Parent x = new Child(); 
x.DoSomething(); 

好的,因为它是常见且易于理解的:

Child x = new Child(); 
x.DoSomething(); 

真正糟糕的一个选择是使用Parent只有Child一种方法DoSomething()。这很糟糕,因为它错误传达了意图:

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

现在,让我们详细说明该问题专门询问的情况:

Parent x = new Child(); 
x.DoSomething(); 

让我们将后半部分作为函数调用,以不同的方式设置此格式:

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

可以简化为:

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

在这里,我认为它WhichType应该是使函数正常工作的最基本/最抽象的类型(除非存在性能问题),这已被广泛接受。在这种情况下,正确的类型应该是ParentParent从中派生出。

这条推理路线说明了为什么使用类型Parent是一个不错的选择,但是不会因其他选择不好(它们不是)而陷入困境。


1

TL;博士 - 使用Child以上Parent是在局部范围内是优选的。它不仅有助于提高可读性,而且还必须确保重载的方法解析正常工作并有助于实现高效的编译。


在当地范围内

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

从概念上讲,它与维护尽可能多的类型信息有关。如果我们降级到Parent,我们实际上只是在剥离可能有用的类型信息。

保留完整的类型信息具有四个主要优点:

  1. 向编译器提供更多信息。
  2. 向读者提供更多信息。
  3. 更干净,更标准化的代码。
  4. 使程序逻辑更易变。

优势1:向编译器提供更多信息

表观类型用于重载方法解析和优化中。

示例:重载的方法解析

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

在上面的示例中,两个foo()调用均有效,但在一种情况下,我们获得了更好的重载方法解析。

示例:编译器优化

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

在上面的示例中,两个.Foo()调用最终都调用了return的相同override方法2。只是,在第一种情况下,有一个虚拟方法查找来找到正确的方法。在第二种情况下不需要此虚拟方法查找,因为该方法的sealed

感谢@Ben,回答中提供了类似的示例

优势2:给读者更多信息

知道确切的类型(即)Child,可以为正在阅读代码的任何人提供更多信息,从而使查看程序的工作变得更加容易。

当然,也许这并不重要,因为无论是实际的代码parent.Foo();child.Foo();有意义,但对于某人来说看到的第一次代码的详细信息,只是普通的帮助。

此外,根据您的开发环境,IDE可能会提供Child比以下工具更有用的工具提示和元数据:Parent

优势3:更清洁,更标准化的代码

我最近看到的大多数C#代码例子,var基本上是简写Child

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

看到一个非var声明语句看起来很像。如果有某种情况下的原因,那太棒了,但否则看起来是反模式的。

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

优势4:更具可变性的原型程序逻辑

前三个优势是最大的优势。这是更多的情况。

不过,对于像其他人一样使用代码的人也使用Excel,我们会不断更改代码。也许我们不需要Child版本的代码中调用唯一的方法,但是稍后我们可能会重新使用代码或重新编写代码。

强类型系统的优点在于,它为我们提供了有关程序逻辑的某些元数据,从而使可能性更加明显。这在原型制作中非常有用,因此最好将其保留在尽可能的位置。

摘要

使用Parent混乱的重载方法分辨率,会抑制某些编译器优化,从读取器中删除信息,并使代码更丑陋。

使用var确实是必经之路。它快速,干净,有模式,可帮助编译器和IDE正确完成其工作。


重要提示:这个答案是关于ParentChild在方法的局部范围。对于返回类型,参数和类字段,Parentvs。的问题Child非常不同。


2
我要补充一点,Parent list = new Child()这没有多大意义。直接创建对象的具体实例的代码(与通过工厂,工厂方法等进行的间接访问相反)具有有关所创建类型的完美信息,它可能需要与具体接口进行交互才能配置新创建的对象等。本地范围不是您获得灵活性的地方。当您使用更抽象的接口(工厂和工厂方法是一个很好的例子)向类的客户端公开新创建的对象时,就可以实现灵活性
crizzis

“在本地范围内,首选在父级上使用子级而不是子级。这不仅有助于提高可读性,”-不一定……如果您不使用子项的详细信息,则只会添加不必要的更多细节。“但是也有必要确保重载的方法解析正常工作并有助于实现高效的编译。” -这在很大程度上取决于编程语言。有很多没有任何问题。
AnoE

1
您是否有资料来源Parent p = new Child(); p.doSomething();Child c = new Child; c.doSomething();行为不同?也许这是某种语言的怪癖,但这应该是对象控制行为,而不是引用。那就是继承的美!只要您可以信任子实现来履行父实现的合同,那么您就很好了。
corsiKa

@corsiKa是的,有几种可能。两个快速的示例是方法签名被覆盖的地方(例如,new在C#中使用关键字),并且当存在多个定义时(例如在C ++中具有多个继承)
纳特

@corsiKa只是一个快速的第三个示例(因为我认为这是C ++和C#之间的巧妙对比),C#存在类似于 C ++ 的问题,您还可以从显式接口方法获得此行为。有趣的是,诸如C#之类的语言使用接口而不是多继承来部分避免这些问题,但是通过采用接口,它们仍然成为问题的一部分-但是,还不算太糟,因为在那种情况下,它仅适用当Parent是一个interface
纳特

0

给定的是parentType foo = new childType1();,代码将仅限于在上使用父类型方法foo,但将能够存储foo对类型源自于的任何对象的引用,parent而不仅仅是对象的实例childType1。如果新childType1实例是唯一foo拥有引用的对象,则最好将foo类型声明为childType1。另一方面,如果foo可能需要保留对其他类型的引用,则将其声明为as parentType将使之成为可能。


0

我建议使用

Child x = new Child();

代替

Parent x = new Child();

后者会丢失信息,而前者则不会。

您可以使用前者来做所有事情,而可以使用后者来做,反之则不行。


为了进一步详细说明指针,让我们拥有多个继承级别。

Base -> DerivedL1 ->DerivedL2

您想将的结果初始化new DerivedL2()为变量。使用基本类型使您可以选择使用BaseDerivedL1

您可以使用

Base x = new DerivedL2();

要么

DerivedL1 x = new DerivedL2();

我看不出有任何一种逻辑方法可以优先选择一种方法。

如果您使用

DerivedL2 x = new DerivedL2();

没有什么可争论的。


3
如果您不需要做更多的事情,那么就可以减少工作是一件好事,不是吗?
彼得(Peter

1
@Peter,做事少的能力不受限制。总是有可能的。如果做更多事情的能力受到限制,那可能就是一个痛处。
R Sahu

但是丢失信息有时是一件好事。关于等级制度,大多数人可能会投票支持Base(假设它可行)。您首选最具体的,而AFAIK则首选最不具体的。我想说的是,对于局部变量而言,这无关紧要。
maaartinus

@maaartinus,令我感到惊讶的是,大多数用户更喜欢丢失信息。我没有看到像其他人那样清楚失去信息的好处。
R Sahu

声明时,Base x = new DerivedL2();您会受到最大的束缚,这使您可以以最小的努力转换另一个(大)孩子。而且,您立即看到有可能。+++我们是否同意void f(Base)比这更好void f(DerivedL2)
maaartinus

0

我建议始终将变量返回或存储为最特定的类型,但接受参数作为最广泛的类型。

例如

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

参数是List<T>,是非常通用的类型。ArrayList<T>LinkedList<T>其他都可以被此方法接受。

重要的是LinkedHashMap<K, V>,返回类型不是Map<K, V>。如果有人想将此方法的结果分配给a Map<K, V>,他们可以这样做。它清楚地表明,此结果不仅仅是一张地图,而是一张具有定义顺序的地图。

假设此方法返回了Map<K, V>。如果调用者想要a LinkedHashMap<K, V>,则必须进行类型检查,强制转换和错误处理,这确实很麻烦。


强制您接受广泛类型作为参数的逻辑也应适用于返回类型。如果该函数的目标是将键映射到值,则应返回一个MapLinkedHashMap-您只想为LinkedHashMap诸如之类的函数返回a keyValuePairsToFifoMap。除非您有特定原因,否则“更广泛”会更好。
corsiKa 2015年

@corsiKa嗯,我同意这是一个更好的名字,但这只是一个例子。我不同意(通常)扩大您的退货类型更好。您在没有任何理由的情况下人为删除类型信息。如果您设想用户合理地需要回退提供给他们的广泛类型,那么您就对他们不利。
亚历山大-恢复莫妮卡

“没有任何理由”-但是有理由!如果我可以返回一个更广泛的类型,那么它将使重构更加容易。如果某人由于某种行为而需要特定类型,那么我全都是为了返回适当的类型。但是如果我不需要,我不想被锁定为特定类型。我想自由地更改我的方法的细节而不会损害那些使用它的人,并且如果出于某种原因将它从例如LinkedHashMap更改TreeMap为,现在我有很多东西可以更改。
corsiKa

实际上,直到最后一句话我都同意。您从LinkedHashMap更改TreeMap为并非易事,例如从更改ArrayListLinkedList。这是语义重要性的变化。在这种情况下,您希望编译器抛出错误,以迫使返回值的使用者注意更改并采取适当的措施。
亚历山大-恢复莫妮卡

但是,如果您只关心映射行为(如果可以的话,应该这样做),这可能是微不足道的更改。如果合同是“您将获得键值映射”并且订单无关紧要(对于大多数地图),那么它是树形图还是哈希图都没有关系。例如,Thread#getAllStackTraces()-返回a Map<Thread,StackTraceElement[]>而不是HashMap特定的,因此他们可以将其更改为所需的任何类型Map
corsiKa
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.