Hello World
Spiga

C#的设计缺陷(2):不能以void作为泛型参数

2012-05-28 12:27 by 老赵, 8197 visits

上一篇文章里我谈了C#中“显示实现接口事件”的限制(不过似乎有点打歪了),这一篇我们换个话题,再来谈泛型方面的限制。相对于Java的假泛型(编译型泛型,类型擦除)来说,真泛型是.NET的一个亮点。Anders Heisenberg多次提到.NET的真泛型有利于编程语言的进一步发展,可以带来更丰富的编程模型。不过.NET支持的泛型是一方面,具体到语言本身则又涉及到编译器的实现,而编译器的实现又收到运行时的限制等等,所以要谈语言的设计缺陷的“原因”就会变得很复杂。不过这里我们就把C#作为一个“成品”来对待,谈下它不允许以void作为泛型参数的“后果”,“原因”则略为一提,不做深究。

泛型的限制

话说C#中泛型是很常用的特性,很多朋友都应该遇到过一些这方面令您不爽的地方。例如,为什么在定义泛型成员的时候,泛型参数T不能限制为Enum(枚举)或Delegate(委托);还有例如,为什么可以限制T存在没有参数的构造函数,但为什么不能指定它有特定参数的构造函数呢?其实很多时候并非是这么做没价值或者做不到,而是如Eric Lippert(咦,怎么老是你)在被问及为什么不支持Enum限制时提到的那样

As I'm fond of pointing out, ALL features are unimplemented until someone designs, specs, implements, tests, documents and ships the feature. So far, no one has done that for this one. There's no particularly unusual reason why not; we have lots of other things to do, limited budgets, and this one has never made it past the "wouldn't this be nice?" discussion in the language design team.

I can see that there are a few decent usage cases, but none of them are so compelling that we'd do this work rather than one of the hundreds of other features that are much more frequently requested, or have more compelling and farther-reaching usage cases. (If we're going to muck with this code, I'd personally prioritize delegate constraints way, way above enum constraints.)

总而言之就是:“有更重要的事情要做啦!”所以我在上一篇文章里也谈过,有些东西虽说可能只是小改动,但可能也再也不会实现了。

void不能作为泛型参数

不过有些问题的确只是些容易绕过的小问题,但我这次要谈的问题造成的麻烦则要大得多:您有没有试过使用void作为泛型类型?有没有想过,假如可以使用void作为泛型参数,会对开发有什么影响?

首先,我们就不需要Func和Action两套委托类型了,因为Func<T1, T2, ..., TN, void>已经能够代替Action<T1, T2, ..., TN>。然后更进一步,很多API就无需写“两套”了——不过真的只要写两套吗?且看Task和Task<TResult>两个类型的ContinueWith重载吧:

class Task
{
    Task ContinueWith(Action<Task>);
    Task<TResult> ContinueWith<TResult>(Func<Task, TResult>);
    Task ContinueWith(Action<Task>, CancellationToken);
    Task ContinueWith(Action<Task>, TaskContinuationOptions);
    Task ContinueWith(Action<Task>, TaskScheduler);
    Task<TResult> ContinueWith<TResult>(Func<Task, TResult>, CancellationToken);
    Task<TResult> ContinueWith<TResult>(Func<Task, TResult>, TaskContinuationOptions);
    Task<TResult> ContinueWith<TResult>(Func<Task, TResult>, TaskScheduler);
    Task ContinueWith(Action<Task>, CancellationToken, TaskContinuationOptions, TaskScheduler);
    Task<TResult> ContinueWith<TResult>(Func<Task, TResult>, CancellationToken, TaskContinuationOptions, TaskScheduler);
}

public class Task<TResult> : Task
{
    Task ContinueWith(Action<Task<TResult>>);
    Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult>);
    Task ContinueWith(Action<Task<TResult>>, CancellationToken);
    Task ContinueWith(Action<Task<TResult>>, TaskContinuationOptions);
    Task ContinueWith(Action<Task<TResult>>, TaskScheduler);
    Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult>, CancellationToken);
    Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult>, TaskContinuationOptions);
    Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult>, TaskScheduler);
    Task ContinueWith(Action<Task<TResult>>, CancellationToken, TaskContinuationOptions, TaskScheduler);
    Task<TNewResult> ContinueWith<TNewResult>(Func<Task<TResult>, TNewResult>, CancellationToken, TaskContinuationOptions, TaskScheduler);
}

首先,如果Task<void>可以代替Task,则已然消灭了其中一半重载。其次,如果可以用Func<Task<TResult>, void>代替Action<Task<TResult>>,则其余重载又可以消减一半。因此没错,一下子就砍掉了四分之三。

其实道理很简单,假如一个API重载,包括返回值和参数在内,总共有N个独立可变类型(即可以选择泛型类型T以及void,且一个参数可能就有多个可变类型),则经过“排列”之后就有2N种可能性,每种都必须单独实现一遍,而这原本只需要一次实现就够了。例如上面的例子有两个独立可变类型TResult和TNewResult,于是需要实现的量就活活变为了4倍。

这是API设计者的噩梦啊。

一则实例

例如,我最近在为Task编写一些扩展,主要是因为在没有C# 5中async/await环境下那么好的语言支持,我只能退而求其次地实现一个Promose模型相关的API,例如最简单的Then,我便要实现四个重载:

static Task Then(this Task task, Func<Task> successHandler);
static Task Then<TResult>(this Task<TResult>, Func<TResult, Task>);
static Task<TNewResult> Then<TNewResult>(this Task, Func<Task<TNewResult>>);
static Task<TNewResult> Then<TResult, TNewResult>(this Task<TResult>, Func<TResult, Task<TNewResult>>);

其实这四个重载做的事情都一样,唯一的区别只是在参数和返回值上各有一个可变类型(还是TResult和TNewResult),导致一个功能要实现四遍。更重要的是其内部实现:

static Task Then(this Task task, Func<Task> successHandler)
{
    var tcs = new TaskCompletionSource<object>(); // 1

    task.ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            tcs.SetException(t.Exception.InnerExceptions);
        }
        else if (t.IsCanceled)
        {
            tcs.SetCanceled();
        }
        else
        {
            Task nextTask;

            try
            {
                nextTask = successHandler(); // 2
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
                return;
            }

            ExecuteAndAssign(nextTask, tcs);
        }
    });

    return tcs.Task;
}

如果您关注这四个方法的实现,就会发现它们的实现几乎完全相同,可以看到的“区别”似乎只是上面标记出的两处。但事实上您会发现,由于类型上无法兼容,导致这些结构相同的代码几乎没法重用,而必须独自写一遍。这就导致了难以避免的Repeat Yourself。对于这种简单逻辑,编写四遍还能勉强接受,但如果是更复杂的逻辑,需要编写八遍呢?此时开发人员就会急切渴望更加强大的泛型系统了。

至于这个问题带来的其他麻烦,例如降低了对函数式编程的支持,让一些编程模式变得复杂等等就不多谈了,会耗费许多笔墨,要引起共鸣就更不容易了……

运行时的限制

使用者方面,让泛型参数支持使用void,对于使用者来说可谓没有任何影响,因为这种“适配”都是由编译器自动完成的。即便是现在,我们在写一个委托对象的时候,也不会指定它的具体类型,编译器会根据最后是否存在返回值类决定究竟是使用Action还是Func重载。有人可能会说,那么对于一些API来说,使用void没有意义啊,例如List<void>,存放void对象?我觉得这没什么问题,让这个List只能存放System.Void类型嘛,它的确没什么意义,但其实我们现在遇到的没意义的情况也太多了,“没意义”的场景程序员自然不会去用,也不会对“有意义”的情况造成不好的影响。

可惜,如今System.Void类型实在是一个特例,它是一个struct,但它存在的目的似乎只是为了支持一些反射相关的API,不能作为泛型参数——您可能会说,不能支持泛型参数类型很多啊,为什么说System.Void是个特例呢?这是因为这点是记录在CLI规范(ECMA-335)里的,没错,的确是运行时规范:

The following kinds of type cannot be used as arguments in instantiations (of generic types or methods):

  • Byref types (e.g., System.Generic.Collection.List`1<string&> is invalid)
  • Value types that contain fields that can point into the CIL evaluation stack (e.g.,List<System.RuntimeArgumentHandle>)
  • void (e.g., List<System.Void> is invalid)

您说System.Void冤不冤,其他两种都可以说是在说一类事物,但第三项完全是指名道姓来的哪。换句话说,泛型不支持void是从运行时开始就存在限制的,并非像Enum之类的只是C#语言的限制。为什么会有这种限制,运行时规范上并没有说清楚。我的猜想是规范考虑到泛型参数会作为返回值来使用,但返回System.Void类型的方法在使用时和普通没有返回值的void方法有所区别?但在我看来,既然您本身就是个虚拟机,那完全是可以适配,“适配”本身也可以是您的职责之一嘛,所以我觉得说到底这原因还是归结为一个字:“懒”。

之前在微博上吐槽这问题的时候,有不少朋友纷纷表示泛型参数不能用void对生活没什么影响。可能有朋友还会奇怪为什么我会有那么多抱怨?我想说,那是因为你没有用过F#等做的好的语言啊,其他如OCaml或Haskell就先不谈了,但F#与C#一样也是一门构建在.NET平台上的语言,它的泛型设计和实现就为C#做出了很好的榜样。不了解也就没抱怨,这情况见的多了。

至于F#是怎么回避System.Void不能作为泛型参数的问题,简单地说就是它使用了自定义的FSharpVoid类型。很显然这不会被C#承认,对互操作不利。这也没办法,谁让这问题出在运行时上呢。在运行时的限制面前,编译器真的无能为力。

相关文章

Creative Commons License

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

Add your comment

33 条回复

  1. 链接

    张志敏 2012-05-28 13:29:11

    正是因为这个原因, 所以 Asp.Net MVC 4.0 在其内部类中定义了 AsyncVoid 结构类型, 来避开 System.Void 类型不能作为泛型参数的问题。

    看来 MVC 团队也很无奈的接受了这个现实!

  2. 链接

    Lucian 2012-05-28 13:40:32

    泛型约束不能用Enum是编译器的限制,IL中是可以的,cecil修改之

  3. 老赵
    admin
    链接

    老赵 2012-05-28 13:57:58

    @张志敏

    是啊,没办法,大家都不爽。

  4. 老赵
    admin
    链接

    老赵 2012-05-28 13:58:22

    @Lucian

    没错,不过这个基本也不会改了,哈哈。

  5. 链接

    NanaLich 2012-05-28 21:46:46

    我觉得这个的根本原因是 void 当作一般的类型来使用时会存在双重含义。 有个很浅显的事实就是,当你在一个函数/方法签名中使用 void 作为返回类型时,你实际上不返回任何东西,也就是说栈上不会为返回值分配空间; 而在 CLI 中即使是没有任何字段的结构体,也不会是 0 体积,sizeof(结构体) 至少会得到 1。

    我认为这种特性的更深层原因是在 C、C++ 乃至 C# 中进行指针和数组操作的时候,地址的偏移量是以目标类型的大小来计算的,比方说 Int32 就是 4 字节、Double 就是 8 字节; 而到了 void 这里就出问题了——void 在栈上不应该占空间,那么以 void* 的偏移单位应该是什么?0 字节吗?这显然不对,开发人员使用 void* 的时候只是不想特别强调目标数据属于什么类型,而非真的希望以 0 字节为单位进行偏移寻址。 实际上用到 void* 的场合,都是以 1 字节为单位进行偏移寻址的。

    那么在这样的情况下,如果声明了类型为 void 的变量或参数,它在栈上所占的字节数应该是 0 还是 1 呢?

    这就是为什么 System.Void 的唯一用途就是在反射中表示 void。

    现在回到主题,作为泛型实参: 如果 void 可以作为泛型实参的话,那么用到对应泛型参数的代码会发生什么呢? 如果代码中有数组方面的操作的话,偏移单位的字节数应该是 0 还是 1?sizeof(T) 应该返回 0 还是 1?栈上分配的空间应该是 0 字节还是 1 字节?

    而最麻烦的部分就在于: 如果在一个简单的计算过程中把 T 类型的数据推到了栈上的话,对于 void 来说,栈顶应该提升 0 字节还是 1 字节? 如果是 1 字节的话会不会跟返回操作的行为有冲突? 如果是 0 字节的话,入栈之后紧随的用到栈上数据的运算操作(比方说,And)会从栈上拿出什么来?

    当问题深入到这里的时候,无论 void 的大小被认为是 0 字节,还是 1 字节,都无法满足所有人、所有代码的需要,所以这部分的行为是未定义的。

    综上所述 CLI 必须阻止你把 void 当作一般的类型来使用,尤其是不能把它用作泛型实参。

    至于 F# 为什么要绕过这个问题,我对 F# 不熟,搞不清具体的缘由。

  6. 老赵
    admin
    链接

    老赵 2012-05-29 10:57:00

    @NanaLich

    不错不错,这其实就是我在文章里说的“返回System.Void类型的方法在使用时和普通没有返回值的void方法有所区别”,行为的确不统一了,但这个虚拟机完全可以解决这方面问题的阿……

    F#绕过这个问题,其实就是因为对于语言设计来说这是很重要的特性,而不是从虚拟机实现上的考虑的。

  7. tokimeki
    203.69.196.*
    链接

    tokimeki 2012-05-29 11:25:45

    從程式語言的發展來看,大多時候是遷就人而非遷就實現的。

    如果 void 在實現上有問題的話,我認為有個可能的解法,就是使其 sizeof 為 1,但是 void 結構變成唯讀。(Mashral 做 P/Invoke 的時候做特別處理)

    當然這樣得作法必須要調整相關的函數實現,但如果一開始在實作 BCL 時就這麼幹,也不是啥大工程。

  8. earthengine
    27.32.228.*
    链接

    earthengine 2012-05-29 19:18:34

    我说一下我在应用中绕过针对Action和Func需要大量重载问题的方法。基本上就是在需要类似的大量泛型的时候,坚持只使用Action系列而不用Function系列。Action系列完全可以模拟Function完成相关功能,缺点只是Action难以构造需要返回值的表达式。但是用Lambda表达式和System.Threading.Task.TaskResultSource可以从Action构造出所需函数,比如说:

    int ObtainIndirectValue(Action<Action<int>> v) {
        var ts = new System.Task.TaskResultSource<int>();
        v(a => ts.SetResult(a));
        return ts.Task.Result;
    }
    

    其它情况可以类推。

  9. earthengine
    27.32.228.*
    链接

    earthengine 2012-05-29 20:08:16

    事实上,C#实现void泛型类型参数并不困难,只要一点点编译器花招即可。在内部,把所有Func<T1,T2,...TN,TResult>重新解释为Action<T1,T2,...TN,Action<TResult>>,当接受Func类型in参数时用Lambda表达式转换为相应Action类型。当返回值或out参数为Func时,用类似上面代码的函数从Action得到Func。

  10. 老赵
    admin
    链接

    老赵 2012-05-29 20:51:54

    @earthengine

    你这“花招”用起来也太麻烦了,C#真要搞还不如F#这种搞个特别的Void,然后编译时做点适配,你这个太伤了,性能开销也大……

  11. 链接

    NanaLich 2012-05-29 22:06:23

    会不会是 F# 所采用的这种手段只适合在 F# 这种语言中使用呢?又或者是设计 C#、CLI 的人还没有找到真正适合所有 CLI 程序的解决办法?

    F# 中有一些功能我虽然能明白是怎么实现的,但我无论如何也想不出放在 C# 中会变成什么样。

    恐怕关于 void 的问题真的是已经复杂到一定境界了吧,即使是在 VES 层面上去解决也是个难题。

  12. 老赵
    admin
    链接

    老赵 2012-05-29 22:20:56

    @NanaLich

    F#这种手段C#当然也能用,只不过就像我所说的那样,解决不了互操作方面的问题。

    其他的就没有明确答案了,谁知道为什么C#和CLI没这么做呢,可能是麻烦,可能是一开始没意识到后来有“更重要”的事情做,可能是缺乏竞争比如JVM也没这么搞Java更是不思进取到家了……

  13. earthengine
    131.170.90.*
    链接

    earthengine 2012-05-30 07:49:21

    @老赵

    其实性能开销不大。真正的缺点是损失了一点类型安全。如果使用Task让你不放心,可以考虑如下变形

    T getValueFromAction<T>(Action<Action<T>> v){
        var value = new ArrayList<T>();
        v(a => value.add(a));
        if(value.Count()>0) return value.item(0);
        throw new ValueNotSetException();
    }
    

    可以看出,当调用v的时候,没有什么东西可以保证你给的Lambda表达式将被执行,而如果没有被执行的话就相当于没有返回值。这在Func的情况下是不允许的。之前Task的版本在此情况下将死锁,等待一个永远不会完成的任务。还要注意一点,我这里采用了ArrayList而不是普通数组,原因是我们不能确保T类型有一个缺省构造函数(甚至不能确认它有任何可访问的构造函数!)。

  14. earthengine
    131.170.90.*
    链接

    earthengine 2012-05-30 08:56:46

    楼主例子的“一行”适配:

    static Task Then(this Task t, Func<Task> f){
        return Then(t,(() => f().ContinueWith(v => new object()));
    }
    
    static Task Then<TResult>(this Task<TResult> t, Func<TResult, Task> f){
         return Then(t, (u => f(u).ContinueWith(v => new object())));
    }
    
    static Task<TNewResult> Then<TNewResult>(this Task t, Func<Task<TNewResult>> f){
        return Then(t.ContinueWith(() => new object()), (u => f()));
    }
    
    static Task<TNewResult> Then<TResult, TNewResult>(this Task<TResult>, Func<TResult, Task<TNewResult>>)
    {
        var tcs = new TaskCompletionSource<TNewResult>(); // 1
    
        task.ContinueWith(t =>
        {
            if (t.IsFaulted)
            {
                tcs.SetException(t.Exception.InnerExceptions);
            }
            else if (t.IsCanceled)
            {
                tcs.SetCanceled();
            }
            else
            {
                Task nextTask;
    
                try
                {
                    nextTask = successHandler(t.Result()); // 2
                }
                catch (Exception ex)
                {
                    tcs.SetException(ex);
                    return;
                }
    
                ExecuteAndAssign(nextTask, tcs);
            }
        });
    
        return tcs.Task;
    }
    
  15. 老赵
    admin
    链接

    老赵 2012-05-30 10:31:51

    @earthengine

    你这个最大的问题其实是把API变得难看且难用了,其次是性能开销和原来的普通调用相比差很多,还不如引入个自己的MyVoid来作为Void来使用,损伤会小很多。

    这个Task实现,主要是Task<T>是继承于Task的,所以Task<T>可以完全当作Task使用,倒也不错。但如果是其它情况比如最单纯的Action和Func就没办法了,要“适配”作的事情会比较多,比如创建各种委托,这还是各种性能开销啊,作为基础类库我还是有点在意的……

    我很不喜欢你用Action来代替Func的策略,因为我是优美API的支持者,API背后可以很丑陋,但是外表(看上去,用起来)一定要好看。

  16. 链接

    NanaLich 2012-05-30 14:46:54

    就像我前面提到的,有些 F# 中的功能,如果放在 C# 中是很难想象的,我认为这跟 F# 作为函数式编程语言所具有的先天的“规划”能力有关——我不是很清楚这个术语到底应该怎么说。 举个简单的例子,我之前在研习 IronJS 的时候就发现 F# 中似乎有类型别名这样的机能,不像 C# 这样你只能在一个 .cs 文件的开头来使用 using 表示,在 F# 中你可以把别名定义在一个 .fs 文件中,然后在另一个 .fs 中直接使用,从这个细节上就可以看出 F# 的编译思想和 C# 是有很大差异的——简单地说,当你遵循 F# 的函数式编程思想去写东西的时候,F# 的编译器可以在很大程度上推测出你的意图(又譬如说你在 F# 中定义和实现的函数是很容易组合的,我觉得搞不好 F# 编译器甚至知道该如何“拆分”),而这种事情对 C# 编译器来说是难以企及的,C# 的编译器不像 F# 那么聪明,而使用 C# 进行开发的人通常也不希望编译器像 F# 编译器那么聪明,那样反而会有一种“失控”的感觉。

    从另一个角度来看,可能有办法能让 C# 也拥有这种功能,可能这种办法可以和 F# 互操作,甚至可能单纯是使用 void 作为泛型参数在 VES 的层面也是可以实现的,但更大的问题是你不知道当你用这种办法写出程序以后别人会对你的程序做什么、别人会期望你的程序具有什么样的行为——当你的全部代码都使用 F# 来编写的时候,F# 编译器有把握确保这样使用 void(在 F# 中似乎是 unit?)不会产生什么意外的结果,但同样的想法在 F# 程序和其它 CLI 语言编写出来的程序、或者全部用其它语言来写的时候,是不适用的。

    关于 F# 使用 FSharpVoid 来绕过这个问题导致对互操作不利,我更倾向于 F# 和 CLR 团队是因为明确知道在互操作时使用 void 作为泛型实参所产生的复杂程度超出了目前任何一个人或团队能 handle 的水平,不具可行性,所以才决定干脆不要去碰 void、用一个别的类型来代替的。

    简而言之,想要在不同语言之间能够完美地互操作的话,必然不能使用 void,而是要用 FSharpVoid,哪怕是 byte 这样的类型来代替 void 的,而且正是因为有着这样的客观事实 CLI 才有限制 void 的使用方式的必要性——直接使用 void 几乎完全不可行(好吧,话不该说那么绝对)。

    我认为这个问题制定 CLI 的人们也一定在想、很努力地在想,但其难度可能不是我们这些没有参与 CLI 设计的人能够理解的。

  17. 老赵
    admin
    链接

    老赵 2012-05-30 15:09:28

    @NanaLich:简而言之,想要在不同语言之间能够完美地互操作的话,必然不能使用 void,而是要用 FSharpVoid,哪怕是 byte 这样的类型来代替 void 的,而且正是因为有着这样的客观事实 CLI 才有限制 void 的使用方式的必要性——直接使用 void 几乎完全不可行(好吧,话不该说那么绝对)。

    这说法肯定不对,FSharpVoid是F#的选择,肯定没法跨语言实现这种规则,F#的规则其他语言为什么遵守啊?用byte倒也罢,但理论上说要在不同语言之间完美互操作,就应该使用System.Void,可惜CLR不支持,也不可能因为F#所谓去对CLR大动干戈,所以F#退而求其次,就跟ASP.NET MVC搞个AsyncVoid一样,既然没有公开标准,只能自己搞一套了。

    其他的,当然从理论上说你说的原因也是有可能的,但这些就没个答案了,谁知道CLI团队是怎么想的,我相信他们一定讨论过,但结论是什么原因是什么没人能知道。所以我只能谈几点可以确定的东西,例如:

    1. C#可以向F#那样做到,只是没做。C#当然也能做到F#的“聪明”,只是没做。
    2. F#里用FSharpVoid,或ASP.NET MVC用AsyncVoid,都只能各自管好自己的,但是对互操作性不利。
    3. CLI也能做到支持System.Void,只是没做。
    4. 本来CLR在展开T的时候就是为不同T各自生成一份Native Code,为void展开一份干净的也没什么特别的。
    5. ……

    至于为什么没做,是“难度大”还是“有更重要的事情要做”还是“无所谓”等等,除非Eric Lippert这种人什么时候说了,我就当轶事一谈,剩下的我就懒得猜了。难度大什么的,不是不可能,但不一定纯粹是技术上的难度,可能是商业或进度上的难度等等,或者是技术债务之类的。在我看来从头写一个CLR虚拟机并实现这点,其实没什么特别的难度嘛。

  18. 链接

    NanaLich 2012-05-30 15:10:39

    说一个小故事: 在 C# 中使用表达式树的时候我偶尔会碰到希望在表达式树中使用赋值(以及其它包含赋值语义的操作)的状况,而 C# 编译器是不允许这么做的。 直观原因就是编译器认为这样的表达式树中存在“副作用”——即改变状态之类的,所以 C# 编译器不允许你这么做。

    可实际上,在 C# 本身是具有赋值操作的,而且还很常见;不仅如此,.NET Framework 的表达式树——或者说抽象语法树本身也是支持赋值操作的,开发人员完全可以利用 Expression.Assign 方法之类的手段来生成这样的表达式项,也可以最终编译成可执行的方法。

    有很长一段时间我都觉得很奇怪,为什么 C# 要限制使用 .NET Framework 支持的功能?

    因为那个时候我没有理解所谓“副作用”对表达式树的起源——Lambda 演算,又或者是“函数式编程”意味着什么。

    F# 作为函数式编程语言,它有意限制了像是赋值语句这样的带有“副作用”的操作的,所以在很少有“副作用”的情况下,F# 拥有比 C# 这样的“传统”编程语言更多的方式来优化在功能上几乎完全等价的代码。

    然而 F# 是要和其它 CLI 程序进行交互的,而其它 CLI 程序所产生的“副作用”对 F# 来说是不可知的,并不是所有可变的东西都会以 Monad 的形式存在,所以 F# 对“副作用”的控制并不如 HasKell 那样完美。

    我认为这也就是为什么即使 F# 有办法解决将 void 用作泛型实参的问题,在和其它语言进行互操作的时候却只能用 FSharpVoid;而在其它语言中,各种各样的在 F# 中很少出现的“副作用”却是随处可见的,即使是在 VES 的层面想解决这个问题也是很艰难的,因此才会有这样的限制。

    不是因为 CLI 限制了 void 的用途才导致 F# 必须用 FSharpVoid 来 workaround,而是因为反正无论如何这样的 workaround 都是必须的,所以 CLI 才会限制 void 的使用。

  19. 链接

    NanaLich 2012-05-30 15:13:38

    当然了,Eric Lippert 的确常常会有一些不太靠谱的话说出来,他说的事情我也不无条件相信。 比方说 Enum 和 Delegate 的事情就是个明显的例子,包括你之前提到的 Attribute 的事情也是,明显是他们懒得思考这个问题了。

    但在 void 能不能用作泛型实参的这个问题上,我认为“不能”是有道理的。 我觉得需要特别特别强调的一点就是,F# 作为函数式编程语言,本身就限制了很多在其它编程中很平常的事情,所以 F# 才可以这样那样。使用包括 C# 在内的较为“传统”的编程语言所编写出来的程序,想要不经过任何调整就适用 F# 中的这些技巧,机会非常渺茫。

  20. 链接

    NanaLich 2012-05-30 15:19:24

    CLI也能做到支持System.Void,只是没做。

    这个可并不确定。

    我觉得这就跟有些人总说“为什么操作系统不能从根本上限制病毒”一样,实际的应用程序开发场合中有太多不应该由所谓的“底层”来进行推测的东西,所以只能拜托大家管好自己份内的事情了。

  21. 链接

    NanaLich 2012-05-30 15:36:26

    话说回来,关于这个限制的问题,别的地方有没有什么比较深入的讨论呢?

    如果有的话不管是哪一种观点都可以找来让大家也参考一下,可能细节上的问题就多少能得出一些结论了。

  22. 老赵
    admin
    链接

    老赵 2012-05-30 15:38:53

    @NanaLich

    目前我只找到一些零零碎碎的讨论,等我有空再找找,如果没有太详细的话,不妨去StackOverflow上开一个,会有很多人把话题收集起来的。

  23. earthengine
    27.32.228.*
    链接

    earthengine 2012-05-30 18:15:38

    关于void的泛型问题,再提供一个例子参考:C++从一开始就支持void作为泛型参数。当然C++的实现方式是多拷贝泛型,有代码膨胀问题,这个问题在CLI中不存在,更不存在于假泛型的Java。

    但是我想说的是,void作为泛型参数,在C++中从一开始就是很平常的事情,而大家都知道C#和Java和C++的渊源。

  24. earthengine
    27.32.228.*
    链接

    earthengine 2012-05-30 20:19:16

    在不改变底层CLI规范的前提下,C#仍然可以有另一种策略,就是模仿C++,当参数类型为void时提供另一种实现。比如List<void>不允许实现,但Task<void>被实现为Task,Func<T,void>被实现为Action<T>,等等。这样,void模板参数适配就仅仅是一种语法糖,但它可以大幅减少需要编写的代码数量(尽管生成的代码量一样)。为了向前兼容且避免生成过多代码,除了void之外的其它类型可以都只有一种实现,就像现在一样。

    这种策略不引入新类型,没有运行时开销,仅仅是生成的代码稍稍多一些而已。

  25. Charley
    122.233.156.*
    链接

    Charley 2012-06-12 21:39:51

    学习了,谢谢。

  26. 飞飞
    120.90.101.*
    链接

    飞飞 2013-06-28 23:08:48

    老赵 前辈。。。最近发现C# 无法获取泛型+ where T:struct的 问题。。。。。乃有什么好方法解决木有。。。

  27. 老赵
    admin
    链接

    老赵 2013-06-28 23:54:23

    @飞飞

    听不懂什么叫做“无法获取泛型+ where T:struct的问题”。

  28. bcdefg
    123.123.114.*
    链接

    bcdefg 2014-06-07 21:08:28

    用T4不就完了?

  29. ahdung
    112.115.147.*
    链接

    ahdung 2015-04-03 12:00:49

    能理解博主的吐槽,我也遇到这个问题了,被重载量生生打败

已自动隐藏某些不合适的评论内容(主题无关,争吵谩骂,装疯卖傻等等),如需阅读,请准备好眼药水并点此登陆后查看(如登陆后仍无法浏览请留言告知)。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我