C# 2.0对现有语法的改进

[自序]

    尽管Microsoft Visual Studio .NET 2005(过去好像叫Visual Studio .NET 2004)一再推迟其发布日期,但广大开发者对其的猜测以及各种媒体对其各方面的“曝光”也似乎已经充斥了网络。但与C#有关的文章似乎无外乎两个方面:VS.NET 2005 IDE特性、介绍C# 2.0中引入的“四大特性(泛型、匿名方法、迭代器和不完整类型)”。对IDE的研究我不想谈了,微软就是微软,他的窗口应用程序总是没得说的。而就语言本身的改进来说,在我听完了Anders Hejlsberg在Microsoft Professional Developers Conference 2003(2003.10, Los Angeles, CA)上的演讲后,发现除了这四大特性之外,还有一些鲜为人知的特性,甚至在微软官方的文档《C# Language Specification Version 2.0》中都没有提到。而这些特性却更加提高了语言的逻辑性。于是我编写了大量实际程序研究了这些特性,终于著成本文。本打算发表在《CSDN开发高手》杂志上的,可无奈水平有限,只能留在这个角落里贻笑大方了。希望能够对那些对C#语言有着浓厚兴趣的朋友有些帮助。

——lover_P 于北京工业大学1号楼221寝室

[修订说明]

  • 2004-08-24
    第一次修订。修改了大量的错别字和文法错误。添加了对global限定符的介绍。

[正文]

    微软在其即将推出的C#2.0(Visual C# Whidbey)中,添加了许多令程序员感到振奋的新特性。除了泛型(Generic)、迭代器(Iterator)、匿名方法(Anonmynous)和不完整类型(Partial Type)等重大的改进,还对现有的语法细节进行了很大的改进,极大地方便了.NET框架程序设计的工作,并且进一步加强了C#语言独有的高逻辑性。在本文中,我将向大家介绍一下这些改进。(文中C#指代的是C#1.2及以前的版本,而C#2.0指代的是微软尚未正式推出的C# Whidbey;文章中的所有代码均在版本号为8.00.30703.4的C#编译器下进行了测试,标有*的错误消息得自版本号为7.10.3052.4的C#编译器。)

[内容]

 

静态类

    使用C#进行.NET框架程序设计的人应该都知道,无法将一个类声明为静态的。例如,下面的类声明:

public static class A {
    static int i;
}

在C#中是无效的,当我们尝试编译这段代码时会得到下面的编译错误*:

error CS0106: 修饰符“static”对该项无效

    由于无法用static修饰符修饰一个类,我们在类中总是能够既声明静态成员又声明实例成员。这无疑会带来很大的灵活性。但是,如果我们希望一个类是静态的,也就是希望强制要求这个类中的所有成员都应该为静态的,就无能为力了,唯一能做的就是自己注意将所有的成员声明为static。当我们忘记对一个本应是静态的成员使用static修饰符(尽管这是一个“低级错误”,但仍有可能发生)时,将会产生难以预料的错误。最重要的是,对于一个逻辑上的静态类(所有成员均使用static修饰符进行声明的类),我们甚至可以声明该类的一个变量并使用new操作符产生该类的实例!这显然不是我们所期望的。

    而在C#2.0中,则提供了静态类这一概念,允许static修饰符对类进行修饰,上面的代码得以通过编译。如果一个类声明中包含了static修饰符,那么这个类中的所有成员将被强制要求声明为静态的。这时,如果我们故意在类中声明实例成员或是不小心忘记了成员声明中的static修饰符,如下面代码所示:

public static class A {
    int i;
}

则编译器会报告错误:

error CS0708: 'A.i': cannot declare instance members in a static class

同时,如果我们声明该类的变量或是试图建立该类的一个实例时,如下面的代码:

public class Test {
    A a; // error CS0723
    void Foo() {
        a = new A(); // error CS0712
    }
}

则会得到下面的两个编译错误:

error CS0723: Cannot declare variable of static type 'A'
error CS0712: Cannot create an instance of the static class 'A'

    很显然,C#2.0中对静态类的支持极大程度地避免了我们在书写程序中的意外失误,尤其是加强了静态类的逻辑性,提高了代码的可读性。

属性的可访问性限定

    C#为我们提供了相当方便的属性定义,使得我们可以像访问类的公有变量成员那样访问类的属性,但还可以同时得到像访问函数那样的安全性。然而,C#只允许属性的设置动作(set{...})和获取动作(get{...})具有相同的可访问性(由属性声明中的publicinternalprivate等修饰符指定)。那么,当我们希望允许从任何程序集中的类获取一个特定类的属性,但只允许该类所在的程序集或该类的私有成员才能设置该属性时,我们只能将这个属性声明为公有且只读(即使用public修饰符声明但只有get{}域),而内部的或私有的成员只能通过设置与该属性相关的内部或私有的变量成员的值来完成属性的设置工作:

public class A {
    int _intValue; // 与属性相关的一个int类型的成员变量

    // 公有且只读的属性,允许任何类获取该属性的值:
    public int Value {
        get { return _intValue; }
    }

    // 下面的方法需要设置上面的属性,
    // 但只能通过访问私有成员变量来完成,
    // 并且要另外进行错误处理
    private void SomeMethod() {
        int i;
        // ......
        // 下面的if-else语句仅用来设置属性值:
        if(0 < i && i < 10) {
            _intValue = i;
        }
        else {
            // 错误处理
        }
    }
}

    很明显,这种做法是非常麻烦的。如果在多个地方改变了成员变量的值会使代码变得冗长不可读,还很有可能会产生错误,譬如该类有另外一个方法:

private void AnotherMethod() {
    int i;
    // ......
    // 下面的if-else语句仅用于设置属性值,
    // 但其对i的区间检测发生了错误
    if(0 < i && i <= 10) {  // 注意这里的 <= 运算符
        _intValue = i;
    }
    // 并且没有进行错误处理
    // ......
}

    上面的方法对将要赋给私有变量成员的值的检查区间是错误的,这种错误是很有可能发生的。一旦调用了这个方法,_intValue很有可能具有错误的值,而访问了Value属性的外部程序集将会出现逻辑错误。这种错误的解决是相当困难的。并且,如果一个小组中的其他成员负责设计同一程序集中其他的类,要求他们在方法中书写如此大量的代码并要进行错误检查是不人道的。

    当然,我们可能会想到将这种设置属性值的工作放到一个内部方法中集中进行:

// 程序集内部的类或该类的私有成员通过
// 下面的内部方法对上面的属性进行设置工作
internal void _setIntValue(int newValue) {
    if(0 < newValue && newValue < 10) {
        _intValue = newValue;
    }
    else {
        throw new System.InvalidArgumentException (
            “The new value must greater than 0 and less than 10”
        );
    }
}

// 下面的方法需要对上述属性进行设置
private void SomeMethod() {
    int i;
    // ......
    _setIntValue(i);  // 通过调用内部方法进行
}

    这样做虽然避免了逻辑错误的出现(至少使出现了错误时的解决工作变得容易),但其可读性仍然不理想,尤其是逻辑性很差,与“属性”本身的意义相去甚远。

    然而C#2.0允许我们对属性的get{}set{}域分别设置可访问性,我们能够将上面的代码简单地写作:

public class A {
    int _intValue;  // 与属性相关的一个int类型的成员变量

    // 公有的属性,
    // 允许任何类获取该属性的值,
    // 但只有程序集内部的类和该类中的私有成员
    // 能够设置属性的值
    public int Value {
        get {
            return _intValue;
        }
        internal set {
            if(0 < value && value < 10) {
                _intValue = value;
            }
            else {
                throw new System.InvalidArgumentException (
                    “The new value must greater than 0 and less than 10”
                );
            }
        }
    }  // property

    // 下面的方法需要对上述属性进行设置
    private void SomeMethod() {
        int i;
        // ......
        Value = i;
    }
}

    尤其在程序集中的其他类的成员中访问该属性时相当方便:

// 这是同一个程序集中另外的一个类:
public class B {
    public A SomeMethod() {
        A a = new A();
        a.Value = 8;  // 这里对属性进行设置,方便!
        return a;
    }
}

    可以看出,能够对属性的获取和设置操作分别设置可访问性限定极大地增强了C#程序的可读性和语言逻辑性,写出的程序也具有更强的可维护性。

命名空间别名

    在C#中,使用类(如声明成员变量或调用静态方法等)的时候需要指定类的完全名称,即命名空间前缀加类的名字。如果我们要在控制台上打印“Hello, world!”,则需要写:

System.Console.WriteLine(“Hello, world!”);

其中,System是命名空间,Console是类的名字,WriteLine()是我们要调用的方法。

    这样的要求显然会使代码变得异常冗余。因此,C#为我们提供了using关键字(指令)。通过使用using指令,我们可以向编译器指定一系列命名空间,当程序中出现了类名字时,编译器会自动到这些命名空间中查找这个类。因此,上面的代码可以写作:

using System;
// ......
public class Test {
    public static void Main() {
        Console.WriteLine(“Hello, world!”);
    }
}

    呵呵,这不就是经典的“Hello World”范例么?很显然,这种写法方便得多,可以极大地提高开发效率。

    然而,两个命名空间中很可能具有同名的类,而我们恰好需要用到这些同名的类。这种情况会经常发生。譬如在.net框架类库中就存在有三个Timer类:System.Timer.TimerSystem.Threading.TimerSystem.Windows.Forms.Timer。我们很可能需要两个Timer类:一个System.Timer.Timer用于在后台以固定的时间间隔检查应用程序或系统状态,一个System.Windows.Forms.Timer用于在用户界面中显示简单动画。这时,尽管我们使用了using指令,我们仍然需要在这些类出现时加上命名空间:

using System.Timer;
using System.Windows.Forms;
// ......
public class MainForm : Form {
    System.Timer.Timer CheckTimer;
    System.Windows.Forms.Timer AnimateTimer;
    // ......
}

    这样的程序仍然显得冗长。

    在C#2.0中,using指令的使用得到了扩展,我们可以使用using指令来为命名空间指定一个别名。当我们需要引用这个命名空间时,可以简单地使用它的别名。为命名空间创建一个别名时,我们使用using指令的扩展用法:using Alias = Namespace,即可为命名空间Namespace指定一个别名Alias。而别名和命名空间的使用方法完全相同。当我们引用一个命名空间中的类型的时候,只需要在命名空间后面加一个圆点“.”再跟上类型名称即可;而引用一个别名所代表的命名空间中的类型时,写法是一样的。那么,上面的例子可以写作:

using SysTimer = System.Timer;
using WinForm = System.Windows.Forms;
// ......
public class MainForm : WinForm.Form {
    SysTimer.Timer CheckTimer; // 与命名空间的使用完全相同
    WinForm.Timer AnimateTimer;
    // ......
}

    我们可以看到,这样的代码要简洁得多。

global限定符

    在C#2.0以前,在使用命名空间时还有一个非常细微的问题。这就是C#命名空间的查找方式。考虑下面这个例子:

using System;

namespace MyNamespace {
    namespace System {
        public class MyConsole {
            public void WriteLine(String str) {
                System.Console.WriteLine(str);    // 注意这一行!
            }
        }
    }
}

     这里我在自己的命名空间内声明了一个System命名空间,并在其中模拟了控制台类。我希望它通过调用System.Console类的方法来模拟控制台的行为。这个程序片断是没有语法问题的,当仍然不能通过编译。其主要原因是C#的命名空间作用域和普通变量的作用域规则类似,总是查找最近的声明,并且内部声明可以覆盖外部声明。因此,这段代码中标有注释的一行在编译的时候编译器会提示找不到类MyNamespace.System.Console——它试图在我自己的命名空间里找System.Console类!

    在C#2.0以前,这个问题对于类库的设计者来说是非常头疼的。唯一的解决方法就是尽量在自己的命名空间内不使用全局命名空间中的名字。但是,由于类库开发者众多,难免会出现类似的情况;而且,这样做还会导致既是在自己的命名空间中也不能使用可读性高而又简洁的名字,这无疑伤害了语言的逻辑性和简洁性。

    然而,C#2.0引入了global关键字,允许我们从全局选取命名空间。下面这个图示从.net命名空间的布局说明了global关键字的地位:


图示1:global关键字的地位
在C#1.x中:

  +++ Who's the root? +++
            |
            +- System (namespace)
            |    |
            |    +- Console (class)
            |         +WriteLine (method)
            |    +- Int32  (struct)
            |    +- ...
            |
            +- MyNameSpace (namespace)
                 |
                 +- System ([sub]namespace)
                      + MyConsole (class)

在C#2.0中

          global  (!!!ROOT!!!)
            |
            +- System (namespace)
            |    |
            |    +- Console (class)
            |         +WriteLine (method)
            |    +- Int32  (struct)
            |    +- ...
            |
            +- MyNameSpace (namespace)
                 |
                 +- System ([sub]namespace)
                      + MyConsole (class)


    这样一来,我们就能通过使用global关键字轻易地解决命名空间的冲突问题。上面的例子也就能够重写为:

using System;

namespace MyNamespace {
    namespace System {
        public class MyConsole {
            public void WriteLine(String str) {
                global.System.Console.WriteLine(str);    // 注意这一行!
            }
        }
    }
}

编译器指令

    在我们调试C#程序时,经常会声明一些临时变量用来监测程序状态,并在调试完成后将这些声明删除。而当我们声明了这样的临时变量,在调试过程中却没有用到的时候,我们通常会得到大量的如:

warning CS0168: The variable 'exp' is declared but never used

的警告。然而,我们很清楚这样的警告是无害的。同样,很多其他时候我们也会得到一些警告,但我们不得不从大量的无害的警告中寻找我们需要的错误消息。

    然而,C#2.0为我们提供了一条新的编译器指令:pragma warning,使得我们能够在一段代码中禁止一些我们确认无害的警告(通过指定警告的编号)。以前,这种工作只能由特定的编译器选项(譬如Microsoft Visual C#编译器的/nowarn)或相应的IDE选项(如Microsoft Visual Studio .NET 2003中的项目属性页中的相应选项)来完成。而且,通过编译环境来隐藏警告将导致在编译整个项目或整个源文件的过程中所有相应的警告都会被隐藏。如果我们仅仅知道在某一个代码块中一个警告是无害的,但对于代码的其它部分,我们还是希望看到这个警告消息时,这种做法就无能为力了。这个时候,我们只有通过pragma warning指令来命令编译器仅仅隐藏某一个代码块中相应的警告。我们可以用下列代码来禁止产生上面的例子中所述的“未使用参数”的警告:

public class Test {
    public void SomeMethod() {
        // 下面的编译器指令禁止了“未使用参数”的警告:
#pragma warning disable 0168
        int tempStatus;
        // ......
        // 下面的编译器指令重新允许产生“未使用参数”的警告:
#pragma warning restore 0168
    }
}

    这样,当编译器编译SomeMethod()方法时,将不会产生上述的“未使用参数”的警告,但在编译其它代码段时,仍然会产生该警告,因为我们用#pragma warning restore指令重新打开了该警告。

固定大小缓冲区

    最后,除了上述的一些特性外,C#2.0还提供了“固定大小缓冲区(Fixed Size Buffers)”的新特性。即像C语言那样可以在结构中声明一个固定大小的数组,这通过System.Runtime.CompilerServices.FixedBufferAttribute属性和Fixed关键字实现(参见参考文献第26页):

[System.Runtime.CompilerServices.FexedBuffer]
public struct Buffer {
    public fixed char Buffer[128];
}

    但由于我所使用的编译器尚未支持这一特性,手头又没有相应的资料,在此就不做介绍了。

 

ASP 3.0对ASP 2.0改进

03-07

ASP 3.0对ASP 2.0的改进rnrn下面的一些特征是从2.0版本中改进或升级来的。rnrn1、 缓冲缺省为打开状态rnASP提供可选的输出缓冲。从IIS 4.0开始,这使得脚本执行得更快,并提供对流向浏览器的输出的控制能力。在ASP 3.0这个改进的性能通过改变Reponse.Buffer属性的缺省设置为True而反映出来。缺省状态下缓冲是打开的,这意味着最终输出只有在进程完成时,或脚本调用Response.Flush或Response.End方法时,才送至客户端。rn注意,可以通过设置Response.Buffer属性为False,关闭缓冲。只有这样,才能发送XML格式化输出给客户端,让XML分析器在收到输出后开始工作。也可以使用Response.Flush发送大页面的一部分,这样使用户可以很快看到部分输出。rnrn2、 Response.IsClientConnected的变化rnResponse.IsClientConnected属性可以在没有任何内容发送给客户端的情况下被读取到。在ASP 2.0中,这只在至少有一部分内容被发送后才能返回准确的信息。这一改进解决了IIS必须响应每个客户的请求(即使客户可能已经转移到另一个页面或站点)的问题。同时如客户在3秒内没有再连接,服务器上创建的完整的输出信息将被丢弃。rnrn3、 带有默认文档的查询字符串rn假如一个用户访问一个站点而不提供所请求页面的名字,默认的文档(如存在的话)将被送往客户端。然而假如他们提供了附在URL后面的查询字符串,这在早先的ASP版本中是被忽略的,而在IIS 5.0和ASP 3.0中这个查询字符串将被送到缺省页面。例如,在一个URL为:http://www.wrox.com/store/的目录中缺省页面为default.asp,则下面这两种情况都将名称/值对Code=1274送往default.asp页面:rnhttp://www.wrox.com/store/?code=1274rnhttp://www.wrox.com/store/default.asp?code=1274rnrn4、 服务器端包含文件的安全性rn服务器端的包含文件常用于一些敏感的信息,如数据库连接字符串或其他访问细节。一个虚拟的路径(即URL而不是完整的物理磁盘文件路径)可以用来指定这些文件。在这种情况下,早先的ASP版本不核对文件的安全设置和用户的证书;换句话说,授权(验证后)的用户和匿名的Web服务器帐号都没有与文件的访问控制列表相比较。在IIS 5.0和ASP 3.0,这些证书将被检查以防止非授权访问。rnrn5、 可配置项移到元数据库中rn在IIS 5.0中ProcessorThreadMax和ErrorsToNTLog的注册项被移到元数据库中,所有有ASP可配置参数能够通过Active Directory和Active Directory服务接口(ADSI)在元数据库中修改。rnrn6、 应用程序中的双线程对象的性能rn为了常有多个并发请求的ASP中获得最佳性能,组件应是双线程的(Both-Threaded)——即单线程单元(Single Threaded Apartment,STA)和多线程单元(Multi-Threaded Apartment,MTA),并且支持COM Free-Thread Marshaller(FTM)。不支持FTM的双线程的COM对象假如被存储在ASP Application状态对象中,将导致运行失败。rnrn7、 更早释放COM对象rn在IIS 5.0中,实例化的对象或组件可更早释放。在IIS 4.0中,COM对象只有在ASP处理完一个页面时才能释放。在IIS 5.0中,假如一个COM对象不使用OnEndPage方法,且对象的引用计数达到零,则这个对象在处理完成之前就被释放了。rnrn8、 缺省时ASP允许进程外组件rn定制的本地服务器组件现在可以从IIS中被实例化,而不需要改变数据库的设置。控制本地服务器实例化的元数据库属性AspAllowOutOfProcComponents缺省值为1,在IIS早期版本为0。rnrn9、 COM对象的安全性rnIIS使用了新的由COM+提供的cloaking特性,因此,从ASP实例化的本地服务器应用程序可以运行在原始的客户的安全环境中。在早期版本中,安全环境被指派到本地服务器COM对象,依赖于调用进程的身份。rnrn10、缺省时组件运行在进程外rn 在ASP早期版本中,所有在ASP页面环境中创建的组件缺省时运行在进程内。rn 为在组件的性能和Web服务器安全性之间折衷,对于一个虚拟的应用程序,可以从Properties对话框中Application Protection的三个选项中选择:rn · Low(IIS Process)rn 这种设置的ASP虚拟应用程序可执行文件和组件都运行在Web服务器可执行文件(Inetinfo.exe)的进程(即内存空间)中。因此,Web服务器就有受到可执行文件或组件失败影响的风险,然而这提供了最快的和最少资源的应用程序执行选项。rn · Medium(Pooled)——这是缺省设置rn 这种设置的ASP虚拟应用程序的所有应用程序可执行文件和组件都运行在单个共享的DLLHost.exe实例的进程(即内存空间)中。这保护了Web服务器可执行文件(Inetinfo.exe)免受任何一个可执行文件或组件失败带来的风险。然而,可执行文件或组件的失败会引起DLLHost.exe进程失败,进而所有其他驻留其中的可执行文件和组件也会失败。rn · High(Isolated)rn 这种设置的ASP虚拟应用程序的所有应用程序可执行文件和组件都运行在单个DLLHost.exe实例的进程(即内存空间)中,但是每个ASP应用程序都有自己的DLLHost.exe实例。DLLHost.exe对应用程序而言是独有的,这保护Web服务器可执行文件免受任何一个可执行文件或组件失败带来的问题。微软建议在任何一个Web服务器上最多驻留10个隔离的虚拟应用程序。rn 推荐的配置是:在它们自己的进程中运行对于任务关键的应用程序,即High(Isolated);余下的所有应用程序在一个共享的进程中运行,即Medium(Pooled)。也可设置组成每个虚拟应用程序的脚本和组件的执行权限(Execute Permission),三个选项是:rn1) None:在这个虚拟的应用程序中不能运行脚本或可执行文件。在实际效果上,这提供了一个在必要时快速和简便地禁止一个应用程序的方式。rn2) Scripts only:仅允许脚本文件,诸如ASP、IDC或其他,能够在这个虚拟应用程序中运行,可执行文件不能运行。rn3) Scripts and Executables:允许任何脚本和可执行文件在这个虚拟应用程序中运行。rn

没有更多推荐了,返回首页

私密
私密原因:
请选择设置私密原因
  • 广告
  • 抄袭
  • 版权
  • 政治
  • 色情
  • 无意义
  • 其他
其他原因:
120
出错啦
系统繁忙,请稍后再试

关闭