我们什么时候需要函数隐藏

Technorati 标签: C#,new,方法隐藏

肖建一篇博文引发了我的思考。

我们应该什么时候使用函数隐藏(new关键字,不明白的请移步MSDN)?

遵守规则

且看如下一段代码:

public class Base
{
	public virtual void Method()
	{
		Console.WriteLine("Base: Method()");
	}
}

public class A : Base
{
	public override void Method()
	{
		Console.WriteLine("A: Method()");
	}
}

public class B : Base
{
	public new void Method()
	{
		Console.WriteLine("B: Method()");
	}
}

public class TestCase
{
	public static void Main(string[] args)
	{
		List<Base> testList = new List<Base>();
		testList.Push(new A());
		testList.Push(new B());
		foreach(Base item in testList)
		{
			item.Method();
		}
	}
}

可以看到,Base类中将Method方法声明为虚方法,就是为了今后的扩展的,其子类如果觉得Base类中Method方法的默认实现不合适的话,可以自行override,从而达到扩展的目的。而类B中由于使用了new关键字而不是override,使得在多态调用时调用的仍是基类中的Method而不是子类中的Method。形象的说,由于B没有遵守“游戏规则”,所以B对于Method的扩展不会在通常情况下起作用。

这里插一句,飞林沙提出“那virtual这个关键字就可以废弃了,永远把这个方法给暴露出来就成了,或者使用abstract

对于这个观点,前半句我将在下面说明;而使用abstract就要求子类必须override父类中的方法,而不能使用一种默认的实现,如果只是声明为virtual,子类就可以选择是否覆盖父类的这个方法。

Hack类库

个人认为,只有这一种情况我们可能用到new关键字来进行函数隐藏,且看代码:

public class 鱼
{
	public void Swim()
	{
		Console.WriteLine("摆尾巴游");
	}
}

public class 章鱼 : 鱼
{
	//章鱼没有尾巴,怎么办?
	public void new Swim()
	{
		Console.WriteLine("摆触手游");
	}
}

显然,在设计“鱼”这个类的时候,没有想到会有鱼没有尾巴,但是我们又确实碰见了没有尾巴的“章鱼”(别较真)。

如果“鱼”这个类是在类库中的话,我们就不能修改这个类,于是我们只能用new关键字进行方法隐藏,从而在表面上差不多的情况下使得“章鱼”有正确的Swim效果。也就是说,我认为,只有在类库设计不当,导致在本应该有virtual声明而没有声明的情况下,才需要子类进行方法隐藏。我不知道我这个观点是不是全面,希望大家补充,还有什么情况下可能会用到方法隐藏

但是方法隐藏有这样一个问题(第一段中提出),即在以基类的签名调用子类时调用的仍然是基类的方法(如((鱼)章鱼A).Swim())。也就是说,只有在我们知道一个实例是该子类的时候,我们才能够调用这个新声明的方法。既然如此,我们为什么不干脆声明一个新的方法,而选择隐藏基类的方法呢

讨论原则

需要注意的是,我的问题并不是override和new的区别,而是以下两点:

  1. 方法隐藏的使用时机是什么?
  2. 既然只有在知道类型签名的时候才能够调用新声明的方法,我们为什么不干脆声明一个新方法?

在弄明白我问的是什么的情况下,希望大家能够积极讨论,在讨论的时候,认真阅读其他人的观点,对症下药,不要答非所问,俗称打岔。

补充

首先来看一些我个人认为比较有意义的回复:

引用Rouper:
我也有过一次,不过后来改成显示实现接口来做了。只有在类库的设计太差的时候才会导致非用new不可。感觉有点像是在hack

我觉得如果用了接口的话,那么用显示接口来做是最好的。其实我推荐这种设计方式:<>,<>,<>。

 引用飞林沙:

关键是这样的转换可以避免再重新New一个耗时的大对象。我觉得这是关键

飞林沙的观点在于为什么不使用组合而是使用继承,但是我感觉跟函数隐藏没有什么特别的联系,不知道是不是我理解的不对。

 引用周中豪:

当要求子类B必须具有某个给定方法名的方法供B自身调用或者供其他部分反射,同名方法又已经在父类A中存在时,为了不影响把B视为A时的逻辑,应该用new。不过 这种情况是一种hack,还是不好的设计。

的确如此,但是我想知道是不是有一种应该(适合)使用函数隐藏的情况,如果没有的话,是不是应该禁用这个语言特性。

引用CFan.Net:
呵呵,我也谈谈我的理解吧。
1、什么时候使用这是一个应用的问题,不要从语言层面的技术角度去思考,那样就钻牛角尖了;
2、举一个游戏的例子,Dosomething表示使用武器,默认的基本武器是刀A,人物在地上拣了一个长枪B, 于是他可以使用B.Dosomething,但是当他走到一个狭窄的巷子里,或是其它什么原因暂时受限制,不能用枪B时,这时调用((A)B).Dosomething临时用刀A了,而正常情况则B.Dosomething;

 我认为这个例子明显不能说明问题,这个例子中一个人显然应该拥有若干种武器,什么时候应该用什么武器直接切换就是了,范不着这么别扭。

引用Alex He:
new关键字告诉编译器,子类中的方法虽然与父类方法一样(签名,参数),但是该方法与基类中的方法没有任何关系

 这句话短小精辟的揭示了函数隐藏的本质。

引用诺贝尔:
首先,章鱼不是鱼。因此这个“强硬”的继承,不过是滥用语法的一个特征。语法能支持的,不等于逻辑能通过。
ms设计这个new,自然有他的用意所在。但是我看不到很有意义的设计点。
我个人的看法,这不过是具体编码中的一种妥协,比如,这个方法虽然不行,但是还有其他方法可以,而既然这个方法不行(也就是没有多态意义),那我完全也不用考虑要避免和基类同名,因为这个名字可能还是很能达意的。
当然,我不认为这种妥协是什么好设计,因为这等于放弃了面向对象的哲学,沦为一种复制粘贴,是一种简陋,容易出错的复用代码行为

 观点相同~

下面评论中Rouper的思想个人觉得比较好,希望大家能够多加关注:)

另外加一个诺贝尔的链接:不要new你的函数。虽然个别语句略显混乱,不过总体上来说说明了一些问题。

下面给大家带来Zhenway的精彩回复!

引用Zhenway: new有new的用处,不要一棍子打死
例如:
public class A
{
    //...
    public virtual A Clone()
    {
        //...
    }
}
public class B : A
{
    //...
    public override A Clone()
    {
        //...
    }
}

看起来不错,但用起来哪?

B b1 = new B();
B b2 = (B)b1.Clone();

是不是很别扭?

稍加改造:

public class A
{
    //...
    public A Clone()
    {
        return CloneCore();
    }
    protected virtual A CloneCore()
    {
        //...
    }
}
public class B : A
{
    //...
    public new B Clone()
    {
        return (B)CloneCore();
    }
    protected override A CloneCore()
    {
        //...
    }
}

这样用起来就舒服了吧:

B b1 = new B();
B b2 = b1.Clone(); // another instance of B as type B
A a1 = b1;
A a2 = a1.Clone(); // another instance of B as type A

 

引用HCOONa:
@Alex He
我上面是这样说的
我认为你说的这种情况不可能出现,原因如下
1.、如果我使用的是父类的签名,那么你子类即使使用函数隐藏也不会起到任何效果
2、如果我使用的是子类,不知道在.NET2.0上是怎么满足我的要求的。退一步说,就算真的需要改变逻辑,那么说明这个子类的行为特征与父类不相符合,属于一种逻辑上的错误,不应该使用继承
3.补充一下,
而且,为了代码兼容性,我们更应该使用扩展方法,或者添加一个新方法的方式来扩充子类
下面我来解释一下
假设你是一个类库提供者,
对于第一种情况
如果客户是这样使用的:
public class ClassInBLL
{
	private Base base = null;
	public ClassInBLL(Base base) : base(base) {}
}

那么,在子类中使用new关键字根本就不起作用。
对于第二种情况:
原来是这样的:

public class Base
{
	public void Method()
	{
		Console.WriteLine("Method in base");
	}
}

public class A : Base
{
}


并且在客户的代码中是直接使用A的,现在你觉得使用A.Method()时候的Base::Method方法不合用了,那么你需要修改子类,为子类添加一个同名方法Method,以使得在客户的代码不需要改动,只需要重新编译。
现在,我建议进行这样的修改:

public class AA : Base
{
}

public class A
{
	private AA ori_a = new AA()
	public void Method() {}
}


如果你说A必须得继承自Base,那么你那种修改方式也是有问题的。
对于我说的第三点,我建议提供扩展方法来扩展A,而不必修改任何代码,当然,这种方式的前提是,修改Method方法不是必须的,它能够满足部分功能,但是新的需求导致我们需要提供一个新的Method1方法,这个方法可以使用扩展方法满足:)

总结

讨论了这么长时间,我说一下我得出的结论吧,不同意的也不要喷,毕竟大家是可以有不同想法的,不同想法就可以讨论。

我得出的结论有以下两点:

  1. 函数隐藏只是用于上面Zhenway提到的那种,子类由于调用方便,需要声明一个与父类中同名函数返回值不同的函数,而这两个函数除了返回值不同以外,在逻辑上没有差别:)
  2. 其他情况不要使用函数隐藏,因为总有更好的解决方案可以解决而没有函数隐藏带来的副作用:)

2012/11/19补充:

在翻旧文的时候突然看到了这篇文章,Zhenway的回复中的内容,现在可以通过协变性完美的解决:

没有方法级的协变性支持,还是做不到更简洁的实现方式。

2012/11/20补充:

参考资料:Why Not ICloneable<T>?

posted @ 2010-07-11 09:46  HCOONa  阅读(2689)  评论(44编辑  收藏  举报