Hello World
Spiga

.NET中*延迟*特性的几个陷阱

2009-06-08 13:19 by 老赵, 24736 visits

.NET发展至今,其实各处都有“延迟(Lazy)”的痕迹,一个小小的“Laziness”给我们带来了不少灵活性。“延迟”的关键就在于“只在需要的时候处理数据”,老赵曾经在多篇文章中提到了类似的概念,如《高阶函数、委托与匿名方法》及《您善于使用匿名函数吗?》。不过“延迟”本身也会给您带来一些陷阱,某些陷阱您很有可能也曾经遇到过。这篇文章便是总结了延迟特性的集中常见陷阱,并给出应对方案。

重复运算

问题

“延迟”的本意是“减少计算”,但是如果您使用不当,很可能反而会造成“重复计算”。例如,我们首先构建一个方法,它接受一个参数n,返回一个Func<int, bool>对象:

static Func<int, bool> DivideBy(int n)
{
    return x =>
    {
        bool divisible = x % n == 0;
        Console.WriteLine(
            "{0} can be divisible by {1}? {2}",
            x, n, divisible ? "Yes" : "No");
        return divisible;
    };
}

返回的Func<int, bool>对象会根据传入的参数x,返回一个表示x能否被n整除的布尔值。在这过程中,还会向控制台输出一句话,例如:“10 can be divisible by 3? No”。每当看到这句话,则表明“经过了一次判断”。那么您是否知道,下面的代码会输出什么结果呢?

List<int> values = new List<int>();
for (int i = 0; i < 10; i++) values.Add(i);

var divideByTwo = values.Where(DivideBy(2));
var divideByTwoAndThree = divideByTwo.Where(DivideBy(3));
var divideByTwoAndFive = divideByTwo.Where(DivideBy(5));

foreach (var i in divideByTwoAndThree) { }
foreach (var i in divideByTwoAndFive) { }

结果如下:

0 can be divisible by 2? Yes
0 can be divisible by 3? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
2 can be divisible by 3? No
3 can be divisible by 2? No
4 can be divisible by 2? Yes
4 can be divisible by 3? No
5 can be divisible by 2? No
6 can be divisible by 2? Yes
6 can be divisible by 3? Yes
7 can be divisible by 2? No
8 can be divisible by 2? Yes
8 can be divisible by 3? No
9 can be divisible by 2? No
0 can be divisible by 2? Yes
0 can be divisible by 5? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
2 can be divisible by 5? No
3 can be divisible by 2? No
4 can be divisible by 2? Yes
4 can be divisible by 5? No
5 can be divisible by 2? No
6 can be divisible by 2? Yes
6 can be divisible by 5? No
7 can be divisible by 2? No
8 can be divisible by 2? Yes
8 can be divisible by 5? No
9 can be divisible by 2? No

您是否发现,无论是在遍历divideByTwoAndThree和divideByTwoAndFive序列时,都会从原有的values序列里重新判断每个元素是否能够被2整除?这就是.NET 3.5中“Where”的延迟特性,如果您在这里没有意识到这点,就可能会产生重复计算,浪费了计算能力。

解决方案

解决这个问题的方法就是在合适的时候进行“强制计算”。例如:

var divideByTwo = values.Where(DivideBy(2)).ToList();
var divideByTwoAndThree = divideByTwo.Where(DivideBy(3));
var divideByTwoAndFive = divideByTwo.Where(DivideBy(5));

结果就变成了:

0 can be divisible by 2? Yes
1 can be divisible by 2? No
2 can be divisible by 2? Yes
3 can be divisible by 2? No
4 can be divisible by 2? Yes
5 can be divisible by 2? No
6 can be divisible by 2? Yes
7 can be divisible by 2? No
8 can be divisible by 2? Yes
9 can be divisible by 2? No
0 can be divisible by 3? Yes
2 can be divisible by 3? No
4 can be divisible by 3? No
6 can be divisible by 3? Yes
8 can be divisible by 3? No
0 can be divisible by 5? Yes
2 can be divisible by 5? No
4 can be divisible by 5? No
6 can be divisible by 5? No
8 can be divisible by 5? No

此时,在获得divideByTwo序列时,就会立即进行计算,这样在遍历后两者时就不会重复计算1,3,5等元素了。

异常陷阱

问题

请问您是否知道下面的代码有什么问题?

public static IEnumerable<string> ToString(IEnumerable<int> source)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }

    foreach (int item in source)
    {
        yield return item.ToString();
    }
}

如果您没有看出来的话,不如运行一下这段代码:

static void Main(string[] args)
{
    IEnumerable<string> values;
    try
    {
        values = ToString(null);
    }
    catch (ArgumentNullException)
    {
        Console.WriteLine("Passed the null source");
        return;
    }

    foreach (var s in values) { }
}

请问,运行上面的代码是否会抛出异常?从代码的意图上看,在ToString方法的一开始我们会检查参数是否为null,然后抛出异常——这本应被catch语句所捕获。但是事实上,代码直到foreach执行时才真正抛出了异常。这种“延迟”执行违反了我们的实现意图。为什么会这样呢?您可以使用.NET Reflector反编译一下,查看一下yield语句的等价C#实现是什么样的,一切就清楚了。

解决方案

对于这个问题,一般我们可以使用一对public和private方法配合来使用:

public static IEnumerable<string> ToString(IEnumerable<int> source)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }

    return ToStringInternal(source);
}

private static IEnumerable<string> ToStringInternal(IEnumerable<int> source)
{
    foreach (int item in source)
    {
        yield return item.ToString();
    }
}

不妨再去查看一下现在的C#代码实现?

资源管理

问题

由于是延迟执行,一些原本最简单的代码模式可能就破坏了。例如:

static Func<string> ReadAllText(string file)
{ 
    using (Stream stream = File.OpenRead(file))
    {
        StreamReader reader = new StreamReader(stream);
        return reader.ReadToEnd;
    }
}

使用using来管理文件的打开关闭是最容易不过的事情了,不过现在如果您通过ReadAllText(@"C:\abc.txt")方法获得的Func<string>对象,在执行时就会抛出ObjectDisposedException。这是因为原本我们意图中的顺序:

  1. 打开文件
  2. 读取内容
  3. 关闭文件

因为有“延迟”特性,这个顺序已经变为:

  1. 打开文件
  2. 关闭文件
  3. 读取内容

这怎么能不出错?

解决方案

有朋友说,这个容易:

static Func<string> ReadAllText(string file)
{ 
    using (Stream stream = File.OpenRead(file))
    {
        StreamReader reader = new StreamReader(stream);
        string text = reader.ReadToEnd();

        return () => text;
    }
}

的确没有抛出异常了,但是这也丧失了“延迟”的特点了。我们必须让它能够在调用委托对象的时候,才去打开文件:

static Func<string> ReadAllText(string file)
{
    return () =>
    {
        using (Stream stream = File.OpenRead(file))
        {
            StreamReader reader = new StreamReader(stream);
            return reader.ReadToEnd();
        }
    };
}

值得一提的是,using完全可以配合yield语句使用。也就是说,您可以编写这样的代码:

static IEnumerable<string> AllLines(string file)
{
    using (Stream stream = File.OpenRead(file))
    {
        StreamReader reader = new StreamReader(stream);
        while (!reader.EndOfStream)
        {
            yield return reader.ReadLine();
        }
    }
} 

由此也可见C#编译器是多么的强大,它帮我们解决了非常重要的问题。

闭包共享

问题

其实这个问题也已经被谈过很多次了,在这里提一下主要是为了保持内容的完整性。您认为,以下代码结果如何?

List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (var a in actions) a();

它打印出来的结果是10个10,具体原因在《警惕匿名方法造成的变量共享》一文中已经有过描述,概括而来便是:各个action共享一个闭包,导致其中的“i”并不是独立的。

解决方案

解决这个问题的方法,只需让不同闭包访问的值相互独立即可。如:

List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
    int  j = i; // 新增代码
    actions.Add(() => Console.WriteLine(j));
}

foreach (var a in actions) a();

 

关于“延迟”特性,您还有什么看法呢?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

81 条回复

  1. 第一控制.NET
    *.*.*.*
    链接

    第一控制.NET 2009-06-08 13:24:00

    老赵果然高产之王……

  2. 老赵
    admin
    链接

    老赵 2009-06-08 13:26:00

    @第一控制.NET
    后面有篇文章会涉及到Lazy的话题,因此先单独写一篇,然后可以给后文引用。
    其实这篇文章只花了一个小时。

  3. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-06-08 13:37:00

    这使我想起了copy-on-write。

  4. Jon.Hong
    *.*.*.*
    链接

    Jon.Hong 2009-06-08 13:38:00

    好文 ,下次会注意了

  5. 老赵
    admin
    链接

    老赵 2009-06-08 13:38:00

    @DiggingDeeply
    copy on write也是种延迟,很多东西的确是思想相近的。

  6. wolf1118baby
    *.*.*.*
    链接

    wolf1118baby 2009-06-08 14:36:00

    老赵你的函数是用什么写的 我在Vs 2005里为什么无法实现。尤其是哪个
    return x=>{.....}这个?

  7. 老赵
    admin
    链接

    老赵 2009-06-08 14:38:00

    @wolf1118baby
    C# 3.0,兄弟你落后于时代了啊

  8. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-06-08 14:45:00

    这个异常陷阱还真隐蔽啊!~~没有Reflector过,不过我认为,yield return 会使编译器生成状态自动机,第一种做法编译器会将异常处理放到第一个状态中去,所以在foreach中才会抛出异常;第二种 Internal 结合是先检查了再返回一个自动机,所以能符合 Main 的思路。
    是否可以把 Main 改成

    try{ foreach (.....

    这样两种方法都没问题了吧?

  9. Yin.Pu@CQUSoft
    *.*.*.*
    链接

    Yin.Pu@CQUSoft 2009-06-08 14:45:00

    嗯,这些细节的确是要注意的,马上动手实验,呵呵。。。

  10. 老赵
    admin
    链接

    老赵 2009-06-08 14:49:00

    @DiryBoy
    嗯,异常是foreach抛出的,foreach放在try里面,自然就捕获得到了。

  11. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-06-08 14:52:00

    呵呵,C#3.0的延迟特性一直让我郁闷

  12. 老赵
    admin
    链接

    老赵 2009-06-08 14:57:00

    @温景良(Jason)
    我觉得还都是很清晰的……

  13. 文野
    *.*.*.*
    链接

    文野 2009-06-08 15:00:00

    真是不得不佩服老赵的知识深度、效率等。当老赵的这些文章,不要说写了,就是让我看都是来不及看,呵呵,毕竟还是要上班养活自己的。
    老赵的知识深度广度怕是难以企及了,如果老赵有空,谈谈如何学习、如何工作、包括写文章这些方面的时间安排、学习方法等技巧吧。
    现在其实最迷茫的就是安排自己的时间了。

  14. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-06-08 15:08:00

    还有这个闭包共享,还真不知道C#怎么想的,编译器会一直将闭包和它的outer reference保持同步,譬如说,

    int i = 1;
    Func<int> func = () => i;
    i = 5;
    Console.WriteLine(func());

    生成的IL在对 i 赋值后会紧跟一条对 func闭包.i 的赋值,所以会输出 5。但是这两步不是原子操作……

    可能是想保持语义上的一致性吧?让人觉得这个 i 就是那个 i……

  15. 老赵
    admin
    链接

    老赵 2009-06-08 15:12:00

    @DiryBoy
    这个闭包实现,倒是符合“规范”的,比如你用JavaScript试试看,一个效果。

  16. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-06-08 15:28:00

    @Jeffrey Zhao

    更神奇的是再加一句
    Console.WriteLine(i);
    会被翻译成
    Console.WriteLine(func闭包.i);

    不知道如果分两个线程,主线程 i++,另外一个线程在 i 还没来得及更新到闭包的时候 i--,这种情况会不会发生呢?
    [edit]噢,不会……所有操作都在闭包的field中进行……
    // 可怜俺的 Js 只停留在 C 式语法的理解上,对它的对象链以及多变的语法感到头晕,暑假我也想梳理一下。

  17. 老赵
    admin
    链接

    老赵 2009-06-08 15:39:00

    @DiryBoy
    就是这样实现的

  18. 一舟
    *.*.*.*
    链接

    一舟 2009-06-08 15:59:00

    呵呵。有趣,俺也犯過一次錯
    《关于yield一次傻乎乎的应用》:
    http://www.cnblogs.com/KiloNet/archive/2009/04/15/1436705.html

  19. 一舟
    *.*.*.*
    链接

    一舟 2009-06-08 16:05:00

    呵呵,不錯,回去改一下試試!

  20. DiggingDeeply
    *.*.*.*
    链接

    DiggingDeeply 2009-06-08 16:20:00

    --引用--------------------------------------------------
    文野: 真是不得不佩服老赵的知识深度、效率等。当老赵的这些文章,不要说写了,就是让我看都是来不及看,呵呵,毕竟还是要上班养活自己的。
    老赵的知识深度广度怕是难以企及了,如果老赵有空,谈谈如何学习、如何工作、包括写文章这些方面的时间安排、学习方法等技巧吧。
    现在其实最迷茫的就是安排自己的时间了。
    --------------------------------------------------------
    谁都一样,都得挣钱养活自己,这是首要条件。
    至于吃饱了之后干什么?人和人就是不一样的。

  21. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-06-08 16:51:00

    在看首页看文章标题觉得内容不错 点进来看发现又是老赵写的 有点不爽

    :目

  22. 老赵
    admin
    链接

    老赵 2009-06-08 17:15:00

    @DiggingDeeply
    我吃饱了撑的慌,于是写点文章消消食,嘿嘿

  23. 老赵
    admin
    链接

    老赵 2009-06-08 17:16:00

    --引用--------------------------------------------------
    韦恩卑鄙: 在看首页看文章标题觉得内容不错 点进来看发现又是老赵写的 有点不爽
    --------------------------------------------------------
    对人不对事,这可不好啊。

  24. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-06-08 17:54:00

    娃哈哈哈 我被你吐了 老赵功力见长阿

  25. mikelij
    *.*.*.*
    链接

    mikelij 2009-06-08 17:54:00

    yield并不好, 所得与所失不相称, 不推荐使用.

  26. 老赵
    admin
    链接

    老赵 2009-06-08 17:57:00

    @mikelij
    啥叫所得与所失不相称?我倒是没有啥推荐不推荐的,该用的时候就去用。
    比如返回一个IEnmuerable对象时,不需要创建一个新对象,很方便啊,这时候为什么不用?

  27. mikelij
    *.*.*.*
    链接

    mikelij 2009-06-08 19:12:00

    @Jeffrey Zhao
    yeild的所得你已经说了好多。所失就是它有隐含得控制流,就象你所说的陷阱,既然有更可控的方法,为了避免隐藏的麻烦和增加稳定性,我个人认为最好不用。

  28. 老赵
    admin
    链接

    老赵 2009-06-08 20:12:00

    @mikelij
    我文章里说了一点yeild需要注意的地方,已经想不出第二点了,而且这点很容易避免啊。
    我觉得yield没有什么所谓“隐含控制流”,有点上纲上线了,一切都是很直观的,顺序性的,没有稳定性和隐藏的麻烦。把一个复杂的迭代器实现简化到极致了。
    如果是要返回IEnumerable的话,我倒强烈建议使用yield。
    你能不能详细谈一下为什么说yield影响了稳定性和复杂性(比如举一些例子),它除了我说的一点之外,还有什么问题呢?

  29. Kevin Dai
    *.*.*.*
    链接

    Kevin Dai 2009-06-08 23:35:00

    文中最后一部分有关闭包的代码,记得闭包是保存当前的变量值的,在这里是保存当时的i到一个闭包中,但是这里最后执行闭包方法时,i却是10。这里怎么没有保存i的值了,而是在执行时,沿用当前的i的真实值了?

  30. Kolor
    *.*.*.*
    链接

    Kolor 2009-06-09 08:52:00

    关注一下,的确,在开发中经常遇到闭包共享这类神奇的问题,当时怎么想都不明白,现在算是知道了,呵呵

  31. qmxle
    *.*.*.*
    链接

    qmxle 2009-06-09 09:43:00

    其实延迟计算是函数式编程的基本特性之一吧。
    我看.NET Framework逐步引入了很多函数式编程的思想。

  32. 老赵
    admin
    链接

    老赵 2009-06-09 09:50:00

    @qmxle
    不用看,这已经是千真万确的事实了,呵呵。

  33. GWPBrian
    *.*.*.*
    链接

    GWPBrian 2009-06-09 09:55:00

    唉,学习,我都不知道还有return () =>这个。呵呵

  34. GWPBrian
    *.*.*.*
    链接

    GWPBrian 2009-06-09 09:56:00

    --引用--------------------------------------------------
    温景良(Jason): 呵呵,C#3.0的延迟特性一直让我郁闷
    --------------------------------------------------------
    请问一下,延迟特性是3。0中特有的吗?

  35. 老赵
    admin
    链接

    老赵 2009-06-09 09:59:00

    @GWPBrian
    延迟特性是.net 1.0中就可以实现的,.net 3.5中只是大量使用了,C# 3.0的一些语言特性对于使用延迟也非常方便。

  36. GWPBrian
    *.*.*.*
    链接

    GWPBrian 2009-06-09 10:04:00

    @Jeffrey Zhao
    哦,学习。

  37. taia
    *.*.*.*
    链接

    taia 2009-06-09 10:42:00

    --引用--------------------------------------------------
    文野: 真是不得不佩服老赵的知识深度、效率等。当老赵的这些文章,不要说写了,就是让我看都是来不及看,呵呵,毕竟还是要上班养活自己的。
    老赵的知识深度广度怕是难以企及了,如果老赵有空,谈谈如何学习、如何工作、包括写文章这些方面的时间安排、学习方法等技巧吧。
    现在其实最迷茫的就是安排自己的时间了。
    --------------------------------------------------------

    我也想知道,授人与鱼,不如授渔
    NET 的更新比JAVA的更新要快多了,特别是从2。0到3。0
    怎样快速学习,掌握它们?

  38. 老赵
    admin
    链接

    老赵 2009-06-09 10:46:00

    @taia
    其实我写过不少这样的文章的,当时就有人骂“不务正业”,呵呵。
    对于思考性的东西,看来兄弟们的包容性还不够,一有价值观念上的冲突就会开骂。

  39. 老赵
    admin
    链接

    老赵 2009-06-09 12:07:00

    --引用--------------------------------------------------
    Kevin Dai: 文中最后一部分有关闭包的代码,记得闭包是保存当前的变量值的,在这里是保存当时的i到一个闭包中,但是这里最后执行闭包方法时,i却是10。这里怎么没有保存i的值了,而是在执行时,沿用当前的i的真实值了?
    --------------------------------------------------------
    您可以仔细分析一下,这段代码中形成了什么样的闭包,闭包里有哪些东西。

  40. jim.c[未注册用户]
    *.*.*.*
    链接

    jim.c[未注册用户] 2009-06-10 08:55:00

    布局改了,要适应一下

    多谢老赵美文!

  41. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2009-06-10 10:09:00

    哎呀.新的布局好像旁边的东西会分散文章主体的注意力....
    文章还没有看哈.

  42. 老赵
    admin
    链接

    老赵 2009-06-10 10:10:00

    @airwolf2026
    习惯就好,嘿嘿

  43. Allie
    *.*.*.*
    链接

    Allie 2009-06-11 11:43:00

    的确有点不习惯了 FF下面会横向拉伸 。。。。不过好文还是要支持!

  44. 老赵
    admin
    链接

    老赵 2009-06-11 11:45:00

    @Allie
    我测试下来1024宽度下没有问题,你用的是FF的什么版本啊?
    如果的确有这问题,能不能截个图发到邮箱给我看一下呢?

  45. asheng
    *.*.*.*
    链接

    asheng 2009-06-11 14:00:00

    新博皮 影响了我看文章 决定不看完这个文章了

  46. 老赵
    admin
    链接

    老赵 2009-06-11 14:04:00

    @asheng
    新皮肤专为1280宽度及以上的浏览器设计,您一定是属于30%的1024宽度的朋友,呵呵。:P

  47. asheng
    *.*.*.*
    链接

    asheng 2009-06-11 15:48:00

    @Jeffrey Zhao
    是,您竟然想要放弃(或者说影响)30% 的用户...
    汗...
    30%是少还是多呢?我迷糊了

  48. 老赵
    admin
    链接

    老赵 2009-06-11 15:51:00

    @asheng
    30%的用户当然还是可以正常浏览的,只是没有70%的用户那么舒适而已。
    其实我就是为了提高70%用户的舒适程度做的修改,呵呵。
    // 暂时只能委屈那30%的朋友了,不过相信不久以后你们就会放弃1024的分辨率了。

  49. Joyaspx
    *.*.*.*
    链接

    Joyaspx 2009-06-14 23:09:00

    以前我在学Linq和lamda表达式的时候就经常遇到这个问题,当值知道最笨的一个方法就是ToList()就可以避免这个问题,今天看了老赵的文章后觉得分析的确是很深刻。

  50. Joyaspx
    *.*.*.*
    链接

    Joyaspx 2009-06-14 23:13:00

    还有一个疑问,看了你的About觉得你非常有个性,敢于这么直截了当的批判新东方,我也很赞同你的观点,但是……,你不怕他们找上你来么,呵呵

  51. 老赵
    admin
    链接

    老赵 2009-06-14 23:16:00

    @Joyaspx
    等找上我的时候再说吧,呵呵。
    // 咋变新东方了,是北大青鸟。

  52. Joyaspx
    *.*.*.*
    链接

    Joyaspx 2009-06-15 08:50:00

    @Jeffrey Zhao
    呵呵,不好意思,写错了

  53. StrongBird[未注册用户]
    *.*.*.*
    链接

    StrongBird[未注册用户] 2009-06-15 18:55:00

    “异常陷阱”
    这个例子只能说明C#编译器的问题,对yield return的编译不应该影响到整个函数的执行顺序。不过现在既然这样了我们也只好接受,谢谢老赵这个例子的提醒!

  54. 老赵
    admin
    链接

    老赵 2009-06-15 19:08:00

    @StrongBird
    我认为C#编译期的举动是完全正确的,您也可以去观察其他语言的行为,都是一样的。
    因为编译期本身无法了解从哪里开始是延迟,从哪里之前是要立即执行的,除非语言本身再加一个特性来标识。

  55. 过江
    *.*.*.*
    链接

    过江 2009-06-18 21:35:00

    赵老师,请教你个问题,我在使用Developer Express的aspgridview控件的时候,在他的ASPxGridView1_RowDeleting中,我想使用Response.Redirect,但是提示“不能在 Page 回调中调用 Response.Redirect。”错误,请问我怎么才能在这个事件里实现类似Response.Redirect页面跳转的功能呢?请给我提供下解决思路呢,谢谢,不好意思,借这个地方提问。

  56. 老赵
    admin
    链接

    老赵 2009-06-18 21:49:00

    @过江
    使用JavaScript吧。

  57. 过江
    *.*.*.*
    链接

    过江 2009-06-18 23:26:00

    @Jeffrey Zhao,使用Javascript,能说详细点么,Repose.write不能使用。

  58. 老赵
    admin
    链接

    老赵 2009-06-19 21:13:00

    @过江
    就是个客户端执行一段window.location = "...",具体怎么做我也不清楚,您可以朝这个方向试试看。

  59. 文明的天空
    *.*.*.*
    链接

    文明的天空 2009-06-24 12:05:00

    有营养

  60. 菜鸟毛
    *.*.*.*
    链接

    菜鸟毛 2009-08-04 09:00:00

    老赵貌似在迫使我们显示器升级啊,不过这小气的公司可不会这么大方的:)

    您在推崇3.5新特性的时候,殊不知还有公司一定坚持1.1啊,这样的公司我究竟该不该去?

  61. 老赵
    admin
    链接

    老赵 2009-08-04 09:09:00

    @菜鸟毛
    如果你问我的话,当然不该去。
    就算死撑2.0的,我也觉得不该去。

  62. liucr
    *.*.*.*
    链接

    liucr 2010-01-08 17:15:00

    static Func<string> ReadAllText(string file)
    {
    using (Stream stream = File.OpenRead(file))
    {
    StreamReader reader = new StreamReader(stream);
    return reader.ReadToEnd;
    }
    }
    其实,这个方法不是延迟造成的问题,而是这个实现代码根本没有完成这个函数要实现的目的,这个函数返回的委托的实现代码是这样的:
    reader.ReadToEnd
    而真实的目的应该是返回这样的委托实现
    using (Stream stream = File.OpenRead(file))
    {
    StreamReader reader = new StreamReader(stream);
    return reader.ReadToEnd;
    }

    鸡蛋里挑骨头,望老赵海涵。

  63. 老赵
    admin
    链接

    老赵 2010-01-08 17:24:00

    @liucr
    你说的没错,不过我认为这的确是延迟造成的问题。
    延迟执行的是ReadToEnd方法,但是因为延迟,它真正执行的时候会失败。
    如果不延迟,那么自然不会有问题。

  64. liucr
    *.*.*.*
    链接

    liucr 2010-01-13 13:59:00

    static Func<string> ReadAllText(string file)
    {
    using (Stream stream = File.OpenRead(file)) //地址:0x0001
    {
    StreamReader reader = new StreamReader(stream);
    return reader.ReadToEnd; //地址:0x0010 ,read.ReadToEnd地址:0x0A01
    }
    }
    如果我们把Func<string>看作是一个函数指针,就好理解为什么我说是代码错误了。
    这个函数返回值正确值应该是:0x0001,因为我们希望在以后调用的时候是从这里开始运行的。
    而现在函数返回值是:0x0A01,所以这个函数是错的。

  65. 老赵
    admin
    链接

    老赵 2010-01-13 16:17:00

    @liucr
    当然是代码错了,掉入延迟的陷阱就是代码写错了,呵呵。

    var i = 1 + 1;
    return () => i;


    return 1 + 1;

    其实你知道的东西我也知道,我知道的你也知道,只是定性不同而已……
    刚才去搜了一些资料(Expert F#,Real World Haskell),我保留我的看法(就是这个属于“延迟”的陷阱)……

  66. liucr
    *.*.*.*
    链接

    liucr 2010-01-13 16:56:00

    @楼主
    不好意思我又来阐述我的观点了。^_^

    我只是觉得这段代码不应该是“延迟”造成的。为什么呢?
    假设这个是“延迟”造成的错误,那么我们可以通过“强制计算”来取消“延迟”,那么下面的代码能实现我们想要的功能吗?

    ReadAllText(@"C:\abc.txt").Invoke()
    

    这段代码执行后还是会报错,必须要改变ReadAllText内部实现才可以。
    而你举得其他例子均可以采用“强制计算”来解决延迟问题。
    所以,我得出结论ReadAllText的实现代码不是由于“延迟”造成的

  67. 老赵
    admin
    链接

    老赵 2010-01-13 17:04:00

    @liucr
    强制计算应该是这样写的,你的写法也已经延迟了:

    static Func<string> ReadAllText(string file)
    { 
        using (Stream stream = File.OpenRead(file))
        {
            StreamReader reader = new StreamReader(stream);
            string text = reader.ReadToEnd();
            return () => text
        }
    }

  68. liucr
    *.*.*.*
    链接

    liucr 2010-01-13 17:31:00

    呵呵,你写这段就代表同意我的观点了,ReadAllText这个方法的错误不是“延时”造成的,而是他内部实现错误。
    而内部错误的原因是开发人员不知道要返回的是什么内容

  69. 老赵
    admin
    链接

    老赵 2010-01-13 17:41:00

    @liucr
    实在不懂,掉入延迟的陷阱就是错了啊,我又没有它没有内部实现错误,但它还是延迟造成的啊。
    所以我说,对于这个错误,我理解你说的,我理解你说的,但是我们对这个错误的定性不一样。
    刚才我查阅了一些资料,它们和我的说法是一致的,也属于Lazy Evaluation的注意事项。
    关于这个我们就说到这里吧,已经是纯粹的文字游戏了。

  70. liucr
    *.*.*.*
    链接

    liucr 2010-01-14 10:00:00

    下面的代码,在WinForm里没有问题的,但如果在ASP的Web项目里出现下面的代码,那肯定是错误的,那请问,下面的代码的错误原因是ASP的解释器的问题吗?

    if (Code == "")
    {
       MessageBox.Show("请填写代码");
    }
    

    上面的例子在大量开始使用asp的时候,网上可以找到很多的。
    如果你觉得是asp的解释器的问题,那么下面的就不用看。


    ----------------------------------------------------
    我觉得判断是否延迟造成的错误,可以用下面的准则判断,
    如果一段代码错误了,在不改变代码实现的情况下,改变代码的执行顺序,就可以解决这个错误,那么这个错误就可以认为是延迟造成的。
    不知道你判断是否延迟的准则是什么?

    你将
    return reader.ReadToEnd
    

    改为
    string text = reader.ReadToEnd();  
    return () => text  
    

    ,这个代码实现已经变了,他们返回的内容是不一样的。

  71. 老赵
    admin
    链接

    老赵 2010-01-14 13:51:00

    @liucr
    好吧,那么我来满足一下你的要求:其实我这段代码只是下面这种写法的变种,只不过被我提取成方法了。

    Func<string> readText = null;
    using (var reader = ...) {
        readText = reader.ReadToEnd;
    }
    
    readText();

    或者更“符合”你的要求一些:
    using (var reader = ...) {
        Func<string> readText = reader.ReadToEnd;
        ThreadPool.QueueUserItem(_ => readText());
    }

    所以说,这不是一回事情么,我只是把ThreadPool.QueueUserItem换成了return而已,这样便把“执行”给延迟了。
    如果你希望通过“立即执行”来解决问题,那么就把return掉的这句话给“立即执行”一下,自然不会有错了……

  72. liucr
    *.*.*.*
    链接

    liucr 2010-01-14 20:14:00

    好吧,那么我们来看一下你的第一段代码的流程
    第一段代码
    1、定义变量readText
    2、打开文件
    3、给变量readText赋值
    4、关闭文件
    5、执行readText,读取文件内容
    这个是代码的意图,运行结果是和这个符合的,而你说的

    这是因为原本我们意图中的顺序:
    打开文件
    读取内容
    关闭文件


    这个算作为设计意图的话,那么现在的情况是

    设计意图 ≠ 代码意图 = 运行结果
    如果这个还定性为“延迟”的责任的话,我无话可说


    第二段代码没有任何问题,简化以后就没使用委托。
    Func<string> readText = reader.ReadToEnd;  
    
    3     ThreadPool.QueueUserItem(_ => readText());  
    
    

    等价于
    reader.ReadToEnd()
    

  73. 老赵
    admin
    链接

    老赵 2010-01-14 20:43:00

    @liucr
    第二段代码也陷入了延迟的陷阱,没有发现其中的错误?
    嗯,我也无话可说了,那么就别说了吧,保留各自看法挺好的。

  74. liucr
    *.*.*.*
    链接

    liucr 2010-01-14 22:05:00

    @楼主
    第一段代码我那里推论错误了,致使你还是认为是“延迟”的问题?
    第二段代码如果还是陷入了延迟的陷阱,那么是我对ThreadPool.QueueUserItem方法是认识错误了,我认为它会执行委托后,再返回主线程继续执行下去,所以才会得出那个等价结论。如果他和主线程并行的话,那么在多线程编程中会出现一个线程A负责打开文件、关闭文件,另一个线程B则使用线程A中文件句柄来读取数据的吗,并且他们之间没用锁来进行约束? 我没编写过多线程,QueueUserItem是我从百度上搜索了说明后就来回答的

    如果楼主有空的话请回答第一个问题,这个是我对于这篇文章的最后一次回复,打扰了楼主,实在抱歉!

  75. 老赵
    admin
    链接

    老赵 2010-01-14 22:16:00

    @liucr
    我实在听不懂你的意思,其实我始终认为你我都是完全明白的,只是非要钻这个牛角尖,要不就这样吧……

  76. 天天不在
    *.*.*.*
    链接

    天天不在 2010-03-16 15:25:00

    F#中.下面的代码如何才能实现延迟加载啊?

    let dx n x =
        let divisible = x % n = 0
        let result = if divisible then "Yes" else "NO"
        System.Console.WriteLine("{0} can be divisible by {1}? {2}",x,n,result) 
        divisible      
    let divtow = dx 2
    let divthree = dx 3
    let values = [| 0 .. 9 |]
    let divideByTwoAndThree = values |> Array.filter divtow |> Array.filter divthree
    

    是不是要改写Array.filter的才行.

  77. 老赵
    admin
    链接

    老赵 2010-03-16 16:18:00

    @天天不在
    Seq.filter

  78. 天天不在
    *.*.*.*
    链接

    天天不在 2010-03-17 09:28:00

    @Jeffrey Zhao
    谢谢.虽然开始有看到有说Seq和IEnumerator的实现一样.也没怎么在意.还以为Seq只能传[]数据.
    看了一下Seq.filter的实现.原以为会看到yield的用法.这个在F#还不知道怎么用.那想他没用是直接自己来生成一个IEnumerator的类型.不和C#用yield来编译器自己生成的一样.

  79. 老赵
    admin
    链接

    老赵 2010-03-17 09:34:00

    @天天不在
    Seq就是IEnumerable<T>。
    Seq可以yield。

  80. stevey
    116.226.7.*
    链接

    stevey 2012-06-19 16:38:53

    @liucr

    好吧,那么我们来看一下你的第一段代码的流程。第一段代码:

    1. 定义变量readText
    2. 打开文件
    3. 给变量readText赋值
    4. 关闭文件
    5. 执行readText,读取文件内容

    这个是代码的意图,运行结果是和这个符合的,而你说的

    这是因为原本我们意图中的顺序:

    第4点,已经关闭了是无法再读取文件的。这里因为ReadToEnd没有立即执行照成的,返回后,导致访问文件资源对象已释放,所以执行报错。我们是在ReadAllText返回后,才去执行ReadToEnd的,故是延迟照成的

  81. noname
    205.181.240.*
    链接

    noname 2012-12-05 18:08:28

    using 会延迟?不懂了

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我