SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  263 随笔 :: 19 文章 :: 3009 评论 :: 74万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

 

delegate void MyInvoker();

void Test(MyInvoker Invoker,  int SomeOthers)
{
 //... codes here
}

void Test(object DefaultValue, int SomeOthers)
{
 //... codes here
}

Test(null, 1);


对于这样一个代码,大家说说看会调用哪一个函数?答案是delegate的版本。这个跟函数出现的顺序无关,而跟调用重载函数的猜测规则有关。这个规则要求在匹配的情况下,尽可能的使用更为特殊的版本。而对于null来说,任何的class都是适合的(不包括valuetype和struct),而delegate是从object派生出来的,因此编译器就选择了delegate的版本。那好,如果我要用null作为参数调用object版本怎么办?办法有两个:
1、在Test(MyInvoker ...) 函数里面判断Invoker==null,跳转Test(object...)版本。
2、Test( (object) null, ...);

实际上第一个办法里面的跳转,还是要依赖于第二个办法,只是不需要每一个调用都这么写。但是带来了方便的同时,也带来了损失性能的后果,尽管并不是很严重。如果你认为性能非常重要,甚至连这么一句判断都是不可容忍的,同时也不希望每次调用都要多写一个(object)来限定调用的版本,那么我建议你不要用重载,直接把这个版本的函数换一个名字,例如TestObject(object DefaultValue, ...)。

到这里事情还没有结束,我们再添加一个函数:

void Test(Label DefaultValue, int SomeOthers)
{
 //... codes here
}


然后我们仍然使用Test(null, 1)来测试,结果却发现编译错误,对吧?因为Label和delegate之间不是派生和继承关系,而是类似兄弟或者叔叔之类的关系。同样的,如果你有一个Label和ToolBar的版本,编译也是通不过的,这个时候你只好添加一个(Label)之类的强制转换来指定调用的版本。不过实际上也只有null才会有如此麻烦的问题,而对于其它的任何东西都没有这个问题。原因很简单,只有null才是任何引用类型都能够接受的,同时也不知道属于哪一个类型的。有人就会想到另外一个情况了:

object o = null;
Test(o, ...);

假设现在有object、Label、ToolBar三个版本,会不会编译错误呢?答案是不会的,并且你会发现它调用了object的版本。原因是版本判断规则并不关心变量里面的实际内容是什么,只关心这个变量声明成什么类型了。由于这里的o是声明为object,因此自然调用的是object版本的函数,这个和(object)强制转换没有什么区别。也许你就会想到本文最前面提到的两个解决方法,现在这个就应该是第三个。没错,这也是一种办法,但是它既没有第一种方法的方便,同时也没有第二种方法的效率,可谓集百害于一体,所以我就没有这么建议。(噢,当然,还有一种办法就是调整参数的顺序,或者添加一个没有意义的valuetype参数,使得通过参数的位置和排列就能够明显的确定调用的版本是哪一个了。这个方法同样不是很好,因为可能让你的程序看起来不是那么优雅。)

为了检验一下你是否明白了整个规则,我给出另外一个例子。这个例子仍然假设有object、Label、ToolBar三个版本:

Control c = new Label();
Test(c, ...);

答案还是调用了object版本。

为什么我会提出来这个问题呢?因为最近我就遇到了这种情况。在我的程序里面使用的是一个delegate和一个object的版本,我这里用伪代码写一下:

delegate object MyInvoker();

object Test(MyInvoker Invoker,  int Key)
{
  if (HashTable.Contains(Key))
    return HashTable[Key];
  else
  {
    object Result = Invoker();
    HashTable.Add(Key, Result);
    return Result;
  }
}


void Test(object DefaultValue, int Key)
{
  if (HashTable.Contains(Key))
   
return HashTable[Key];
 
else
  {
    HashTable.Add(Key, DefaultValue);
   
return DefaultValue;
  }

}

Test(null, 1);

很明显,我这里是为了缓冲某个数据——如果某个Key所对应的数据不存在,那么要么通过Invoker来返回一个新的实力,要么通过DefaultValue来设置。如果我调用Test(null, 1),那么应该表示如果找不到的话,就设置成null,而不是调用Invoker=null——这明显是非法的,并且没有意义。最后权衡了一下,我还是不用重载的方式,换了一个名字算了。

至于说提出这个问题还有别的原因的,因为有的时候不是编译错误那么简单。比如上面那个例子就是运行时的问题,并且如果这里不是delegate的问题,而是其它类型,那么可能在调用的时候并不知道调用了错误的版本,而等到很后来才出现问题,这时候要找出这个隐含的bug就不是那么简单了。因此我的结论就是,重载的时候需要考虑一下null的问题,并审慎对待重载。
posted on   Sumtec  阅读(1173)  评论(3编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
点击右上角即可分享
微信分享提示