2006年04月25日

正则表达式语法

正则表达式是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为“元字符”)。模式描述在搜索文本时要匹配的一个或多个字符串。

下面是正则表达式的一些示例:

表达式 匹配
/^\s*$/ 匹配空行。
/\d{2}-\d{5}/ 验证由两位数字、一个连字符再加 5 位数字组成的 ID 号。
/<\s*(\S+)(\s[^>]*)?>[\s\S]*<\s*\/\1\s*>/ 匹配 HTML 标记。

下表包含了元字符的完整列表以及它们在正则表达式上下文中的行为:

字符 说明
\ 将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如,“n”匹配字符“n”。“\n”匹配换行符。序列“\\”匹配“\”,“\(”匹配“(”。
^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^
还会与“\n”或“\r”之后的位置匹配。
$ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$
还会与“\n”或“\r”之前的位置匹配。
* 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配“z”和“zoo”。* 等效于 {0,}。
+ 一次或多次匹配前面的字符或子表达式。例如,“zo+”与“zo”和“zoo”匹配,但与“z”不匹配。+ 等效于
{1,}。
? 零次或一次匹配前面的字符或子表达式。例如,“do(es)?”匹配“do”或“does”中的“do”。? 等效于
{0,1}。
{n} n 是非负整数。正好匹配 n
次。例如,“o{2}”与“Bob”中的“o”不匹配,但与“food”中的两个“o”匹配。
{n,} n 是非负整数。至少匹配 n
次。例如,“o{2,}”不匹配“Bob”中的“o”,而匹配“foooood”中的所有 o。’o{1,}’ 等效于 ‘o+’。’o{0,}’ 等效于
‘o*’。
{n,m} mn 是非负整数,其中 n <= m。至少匹配
n 次,至多匹配 m 次。例如,“o{1,3}”匹配“fooooood”中的头三个 o。’o{0,1}’ 等效于
‘o?’。注意:您不能将空格插入逗号和数字之间。
? 当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是“非贪心的”。“非贪心的”模式匹配搜索到的、尽可能短的字符串,而默认的“贪心的”模式匹配搜索到的、尽可能长的字符串。例如,在字符串“oooo”中,“o+?”只匹配单个“o”,而“o+”匹配所有“o”。
. 匹配除“\n”之外的任何单个字符。若要匹配包括“\n”在内的任意字符,请使用诸如“[\s\S]”之类的模式。
(pattern) 匹配 pattern 并捕获该匹配的子表达式。可以使用 $0$9
属性从结果“匹配”集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用“\(”或者“\)”。
(?:pattern) 匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用“或”字符
(|) 组合模式部件的情况很有用。例如,与“industry|industries”相比,“industr(?:y|
ies)”是一个更加经济的表达式。
(?=pattern) 执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern
的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,“Windows (?=95| 98| NT| 2000)”与“Windows
2000”中的“Windows”匹配,但不与“Windows
3.1”中的“Windows”匹配。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
(?!pattern) 执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern
的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,“Windows (?!95| 98| NT|
2000)”与“Windows 3.1”中的“Windows”匹配,但不与“Windows
2000”中的“Windows”匹配。预测先行不占用字符,即发生匹配后,下一匹配的搜索紧随上一匹配之后,而不是在组成预测先行的字符后。
x| y xy 匹配。例如,“z| food”与“z”或“food”匹配。“(z|
f)ood”与“zood”或“food”匹配。
[xyz] 字符集。匹配包含的任一字符。例如,“[abc]”匹配“plain”中的“a”。
[^xyz] 反向字符集。匹配未包含的任何字符。例如,“[^abc]”匹配“plain”中的“p”。
[a-z] 字符范围。匹配指定范围内的任何字符。例如,“[a-z]”匹配“a”到“z”范围内的任何小写字母。
[^a-z] 反向范围字符。匹配不在指定的范围内的任何字符。例如,“[^a-z]”匹配任何不在“a”到“z”范围内的任何字符。
\b 匹配一个字边界,即字与空格间的位置。例如,“er\b”匹配“never”中的“er”,但不匹配“verb”中的“er”。
\B 非字边界匹配。“er\B”匹配“verb”中的“er”,但不匹配“never”中的“er”。
\cx 匹配由 x 指示的控制字符。例如,\cM 匹配一个 Control-M 或回车符。x 的值必须在
A-Z 或 a-z 之间。如果不是这样,则假定 c 就是“c”字符本身。
\d 数字字符匹配。等效于 [0-9]。
\D 非数字字符匹配。等效于 [^0-9]。
\f 换页符匹配。等效于 \x0c 和 \cL。
\n 换行符匹配。等效于 \x0a 和 \cJ。
\r 匹配一个回车符。等效于 \x0d 和 \cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t 制表符匹配。与 \x09 和 \cI 等效。
\v 垂直制表符匹配。与 \x0b 和 \cK 等效。
\w 匹配任何字类字符,包括下划线。与“[A-Za-z0-9_]”等效。
\W 任何非字字符匹配。与“[^A-Za-z0-9_]”等效。
\xn 匹配 n,此处的 n
是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,“\x41”匹配“A”。“\x041”与“\x04”&“1”等效。允许在正则表达式中使用
ASCII 代码。
\num 匹配 num,此处的 num
是一个正整数。到捕获匹配的反向引用。例如,“(.)\1”匹配两个连续的相同字符。
\n 标识一个八进制转义码或反向引用。如果 \n 前面至少有 n 个捕获子表达式,那么 n
是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
\nm 标识一个八进制转义码或反向引用。如果 \nm 前面至少有 nm 个捕获子表达式,那么
nm 是反向引用。如果 \nm 前面至少有 n 个捕获,那么 n 是反向引用,后面跟
m。如果前面的条件均不存在,那么当 n m 是八进制数 (0-7) 时,\nm 匹配八进制转义码
nm
\nml n 是八进制数 (0-3),ml 是八进制数 (0-7)
时,匹配八进制转义码 nml
\un 匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号
(©)。


             我们来看看Reflector这个.NET下的反汇编工具能做到什么程度呢?
             以下我将写一个类,提供一个方法,能对传入的字符串做出数字和长度限制,返回true或false.
             源代码如下:
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;


namespace MySpace
{
    class MyClass
    {
        /// <summary>
        /// input是输入的字符串
        /// count是允许输入的字符串的长度
        /// </summary>
        /// <param name="input"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public static bool Can(string input, int count)
        {
            string pattern = @"[0-9]+";
            if (Regex.IsMatch(input, pattern))
                throw new Exception("字符串不能出现数字");
            if (input.Length > count)
                throw new Exception("字符串长度超过允许范围:" + count);
            return true;

        }
    }
    public class Program
    {

        static void Main(string[] args)
        {
            string[] ss = { "123fkd", "fdkhfjd?", "fdh3,d", ".xckjdfdjfhdjfhdj" };
            foreach (string s in ss)
            {
                try
                {
                    if (MyClass.Can(s, 10))
                        Console.WriteLine(s + " 通过检验.");
                }
                catch (Exception e)
                {
                    Console.WriteLine(s + " " + e.Message);
                }
            }
            Console.Read();
        }
    }
}/
我们将这段代码编译成.EXE或DLL。
——————————–下面我们来看看使用Reflector反汇编此组件后的代码:

namespace MySpace
{
      internal class MyClass
      {
            // Methods
            static MyClass();
            public MyClass();
            public static bool Can(string input, int count);

            // Properties
            public string Pattern { get; set; }

            // Fields
            private static string pattern;
      }

      public class Program
      {
            // Methods
            public Program();
            private static void Main(string[] args);
      }
}


这里它完整显示出了MySpace这个名称空间中的类,以及各个类的成员(字段、属性和方法)
我们在看详细内容:

static MyClass()
{
      MyClass.pattern = "[0-9]+";
}

public string Pattern
{
      get
      {
            return MyClass.pattern;
      }
      set
      {
            MyClass.pattern = value;
      }
}

public static bool Can(string input, int count)
{
      if (Regex.IsMatch(input, MyClass.pattern))
      {
            throw new Exception("\u5b57\u7b26\u4e32\u4e0d\u80fd\u51fa\u73b0\u6570\u5b57");
      }
      if (input.Length > count)
      {
            throw new Exception("\u5b57\u7b26\u4e32\u957f\u5ea6\u8d85\u8fc7\u5141\u8bb8\u8303\u56f4:" + count);
      }
      return true;
}

基本上,除了没有完整显示出中文外(显示的是中文的编码),该有的都有了,基本和源代码完全一样。。。

再看Main()方法内的代码:

private static void Main(string[] args)
{
      string[] textArray1 = new string[] { "123fkd", "fdkhfjd?", "fdh3,d", ".xckjdfdjfhdjfhdj" };
      string[] textArray3 = textArray1;
      for (int num1 = 0; num1 < textArray3.Length; num1++)
      {
            string text1 = textArray3[num1];
            try
            {
                  if (MyClass.Can(text1, 10))
                  {
                        Console.WriteLine(text1 + " \u901a\u8fc7\u68c0\u9a8c.");
                  }
            }
            catch (Exception exception1)
            {
                  Console.WriteLine(text1 + " " + exception1.Message);
            }
      }
      Console.Read();
}

看到这里,是不是觉得Reflector这个工具很变态啊。。。唉,没的办法了,.NET在提供强大功能的同时,不可避免地放弃了一些东西。

                  正则表达式是对字符串进行操作的一套功能强大的工具,下面简要讨论几个问题.

1.正则表达式能做什么?

        简单总结一下,有:测试判断,查找提取,删除,替换字符串的功能.

        1)测试判断:对于一个给定的字符串,我们可以加以测试其是否满足我们的特定字符串的规则的要求.例如,输入一个字符串,我们要对它进行测试,判断其是否是合法有效的IP地址字符串,以保证字符串是我们需要的。

        2)查找提取:对于一个给定的字符串,我们查找出其中所有满足规则的子字符串.比如:我们要在一个字符串中,查找出所有的出现一个名字的位置,并提取满足规则的子字符串另加它用.

        3)删除:对于一个给定字符串,我们要删除其中符合规则的所有子字符串,.如:我们要将一个字符串中所有出现"abc"的子字符串全删除.

        4)替换:对于一个给定字符串,我们要替换其中符合规则的所有子字符串.如:我们要将一个字符串中所有出现"abc"的子字符串全替换成"ABCDE".

2.我们如何使用正则表达式

        这里,我使用的是.NET类库所提供的一个正则表达式类:System.Text.RegularExpressions.Regex

        当然,也有很多其他的组件提供正则判断引擎,只不过名称或方法名有点不同,但是基础的逻辑是一样的.

       关于正则表达式规则字符串的写法,可查询相关资料文档(本blog有相关内容,自行查阅.),我这里只介绍如何使用正则表达式的一些简单示例.

3.一些示例

1)测试判断:使用Regex.IsMatch方法对字符串加以测试判断,看其是否是我们需要的特定格式的字符串.

//这里我使用一个IP正则表达式规则,能保证输入的字符串是IP地址字符串.

using System;
using System.Text.RegularExpressions;
class App
{
 static void Main()
 {
  //这是IP正则表达式规则字符串(…由于是自己写的,可能不够精简…)
  string pattern = @"^([0-2]{0,1}[0-9]{0,1}[0-9]{1}\.){3}[0-2]{0,1}[0-9]{0,1}[0-9]{1}$";
  //简单编写一些测试字符串,如果有兴趣,可以自己增加测试字符串加以测试
  string[] Inputs = { "255.255.255.0", "127.0.0.1", "djhfdj", "1272.2.2.1", "127.0.0.1a" };
  foreach (string s in Inputs)
  {
   if (Regex.IsMatch(s, pattern)) //只需要这样一个方法调用,就可以自动判断出是否符合规则
    Console.WriteLine(s+" 是正确的IP地址.");
   else
    Console.WriteLine(s + " 不是正确的IP地址.");
  }
  Console.Read();
 }
}

2))查找提取:使用Regex.Match或Regex.Matchs方法查找并提取满足规则的子字符串

<1>//使用Regex.Match方法返回第一个满足正则判断规则的子字符串
using System;
using System.Text.RegularExpressions;

class Program
{
 static void Main()
 {
  string pattern = @"abc";
  //字符串Input是你需要加以操作的目的字符串
  string Input = "fdkabcslabcd";
  //使用Regex.Match方法,将从字符串Input中查找出第一个匹配pattern表达规则的子字符串
  //并返回一个Match对象,此Match对象的Index就是在Input字符串中被找到的满足规则的子字符串索引位置,
  //而Value属性就是满足规则的子字符串的被复制的子字符串
  Match m = Regex.Match(Input, pattern);
  if (m.Success)       //判断是否成功
  {
   Console.WriteLine("在原字符串中的索引位置 "+m.Index + " 查找的结果 " + m.Value);  }
  else
   Console.WriteLine("没有需要查找的字符串.");

  Console.Read();

 }
}

<2>使用Regex.Matchs方法返回所有满足规则的子字符串

using System;
using System.Text.RegularExpressions;

class App
{
 static void Main()
 {
  string pattern = @"abc";
  string Input = "dfkhabcfkdjabcek";
  MatchCollection ms = Regex.Matches(Input, pattern);
  foreach(Match m in ms)
  {
   Console.WriteLine("在原字符串中的索引位置 " + m.Index + " 查找的结果 " + m.Value);
  }
  Console.Read();
 }

}

3)删除:使用Regex.Replace方法,将查找出来的子字符串替换成"",其实就是删除.

using System;
using System.Text.RegularExpressions;

class App
{
 static void Main()
 {
  string pattern = @"abc";
  string Input = "dfkhabcfkdjabcek";
  
  string s = Regex.Replace(Input, pattern,"");
  Console.WriteLine("删除前:" + Input);
  Console.WriteLine("删除后:"+s);
  Console.Read();
 }

}
4)替换:使用Regex.Replace方法,将查找出来的子字符串替换成指定字符串,就是替换啦
using System;
using System.Text.RegularExpressions;

class App
{
 static void Main()
 {
  string pattern = @"abc";
  string Input = "dfkhabcfkdjabcek";
  //直接使用Replace方法指定需要替换的字符串和要替换的字符串
  string s = Regex.Replace(Input, pattern,"|ABC|");
  Console.WriteLine("替换前:" + Input);
  Console.WriteLine("替换后:"+s);
  Console.Read();
 }

}

2006年04月23日

#include <time.h>         //必须引如此头文件
#include <iostream>
using namespace std;

void main(){
    time_t t;                     //时间变量
    srand((unsigned)time(&t));   //种下随即发生器必须的种子
    for (int i=0; i<10; i++)
    {
        cout<<rand()<<endl;      //rand()方法根据前面的设置,返回随即数
    }
    cin.get();
}

#include <time.h>
#include<algorithm>    //generate方法的使用需要此STL算法头文件
#include <iostream>
using namespace std;

void main(){
    int data[10];
    //将随即生成的数写入data数组
    time_t t;    //获取当前时间变量
    srand((unsigned)time(&t));    //种下随即发生器必须的种子
//generate通用算法其实就是将函数rand产生的返回值依次迭代添满data数组
    generate(&data[0],&data[9],rand);//rand是一个全局函数,这里传入此函数的函数指针
    for (int i=0; i<10; i++)
        cout<<data[i]<<endl;
    cin.get();
}

2006年04月19日

2)编程找出1000以内的满足勾股定理            的整数组(a,b,c)。
//一个垃圾算法,勉强凑合可以用用。。。
#include <iostream>
#include <math.h>
using namespace std;

int go()
{
    int count = 0;
    for (int i=1; i<=1000; i++)
    {
        if (i*i*2>1000*1000)break;
        for(int j=i+1; j<=1000; j++)
        {
            double dr2 = i*i+j*j;
            int r2 = i*i+j*j;
            if (r2 > 1000*1000)break;
            for (int k=sqrt(dr2); k<=1000; k++)
            {
                if (r2 == k*k)
                {
                    count++;
                    cout<<i<<" "<<j<<" "<<k<<endl;
                    break;
                }
            }
        }
    }
    return count;
}
int main()
{
    cout<<"一共有 "<<go()<<" 对满足勾股定理的数"<<endl;;
    cin.get();
    return 0;
}

 

创建型 结构型 行为型
Factory Method Adapter_Class Interpreter
Template Method
对象 Abstract Factory
Builder
Prototype
Singleton
Adapter_Object
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Chain of Responsibility
Command
Iterator
Mediator
Memento
Observer
State
Strategy
Visitor
概览




名称 Factory Method
结构
意图 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
适用性
  • 当一个类不知道它所必须创建的对象的类的时候。
  • 当一个类希望由它的子类来指定它所创建的对象的时候。
  • 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。




名称 Abstract Factory
结构
意图 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
适用性
  • 一个系统要独立于它的产品的创建、组合和表示时。
  • 一个系统要由多个产品系列中的一个来配置时。
  • 当你要强调一系列相关的产品对象的设计以便进行联合使用时。
  • 当你提供一个产品类库,而只想显示它们的接口而不是实现时。




名称 Builder
结构
意图 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
适用性
  • 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
  • 当构造过程必须允许被构造的对象有不同的表示时。




名称 Prototype
结构
意图 用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
适用性
  • 当要实例化的类是在运行时刻指定时,例如,通过动态装载;或者
  • 为了避免创建一个与产品类层次平行的工厂类层次时;或者
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。




名称 Singleton
结构
意图 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
适用性
  • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
  • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时。




名称 Adapter
结构

意图 将一个类的接口转换成客户希望的另外一个接口。A d a p t e r 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
适用性
  • 你想使用一个已经存在的类,而它的接口不符合你的需求。
  • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作。
  • (仅适用于对象A d a p t e r )你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口。对象适配器可以适配它的父类接口。




名称 Bridge
结构
意图 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
适用性
  • 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
  • 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时B r i d g e 模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
  • 对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译。
  • (C + +)你想对客户完全隐藏抽象的实现部分。在C + +中,类的表示在类接口中是可见的。
  • 有许多类要生成。这样一种类层次结构说明你必须将一个对象分解成两个部分。R u m b a u g h 称这种类层次结构为“嵌套的普化”(nested generalizations )。
  • 你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。一个简单的例子便是C o p l i e n 的S t r i n g 类[ C o p 9 2 ],在这个类中多个对象可以共享同一个字符串表示(S t r i n g R e p )。




名称 Composite
结构
意图 将对象组合成树形结构以表示“部分-整体”的层次结构。C o m p o s i t e 使得用户对单个对象和组合对象的使用具有一致性。
适用性
  • 你想表示对象的部分-整体层次结构。
  • 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。




名称 Decorator
结构
意图 动态地给一个对象添加一些额外的职责。就增加功能来说,D e c o r a t o r 模式相比生成子类更为灵活。
适用性
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 处理那些可以撤消的职责。
  • 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。




名称 Facade
结构
意图 为子系统中的一组接口提供一个一致的界面,F a c a d e 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
适用性
  • 当你要为一个复杂子系统提供一个简单接口时。子系统往往因为不断演化而变得越来越复杂。大多数模式使用时都会产生更多更小的类。这使得子系统更具可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难。F a c a d e 可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过f a c a d e 层。
  • 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入f a c a d e 将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
  • 当你需要构建一个层次结构的子系统时,使用f a c a d e 模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过f a c a d e 进行通讯,从而简化了它们之间的依赖关系。




名称 Flyweight
结构
意图 运用共享技术有效地支持大量细粒度的对象。
适用性
  • 一个应用程序使用了大量的对象。
  • 完全由于使用大量的对象,造成很大的存储开销。
  • 对象的大多数状态都可变为外部状态。
  • 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
  • 应用程序不依赖于对象标识。由于F l y w e i g h t 对象可以被共享,对于概念上明显有别的对象,标识测试将返回真值。




名称 Proxy
结构
意图 为其他对象提供一种代理以控制对这个对象的访问。
适用性
  • 在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用P r o x y 模式。下面是一 些可以使用P r o x y 模式常见情况:
    1) 远程代理(Remote Proxy )为一个对象在不同的地址空间提供局部代表。 NEXTSTEP[Add94] 使用N X P r o x y 类实现了这一目的。Coplien[Cop92] 称这种代理为“大使” (A m b a s s a d o r )。
    2 )虚代理(Virtual Proxy )根据需要创建开销很大的对象。在动机一节描述的I m a g e P r o x y 就是这样一种代理的例子。
    3) 保护代理(Protection Proxy )控制对原始对象的访问。保护代理用于对象应该有不同 的访问权限的时候。例如,在C h o i c e s 操作系统[ C I R M 9 3 ]中K e m e l P r o x i e s 为操作系统对象提供 了访问保护。
    4 )智能指引(Smart Reference )取代了简单的指针,它在访问对象时执行一些附加操作。 它的典型用途包括:
  • 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为S m a r tP o i n t e r s[ E d e 9 2 ] )。
  • 当第一次引用一个持久对象时,将它装入内存。
  • 在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。




名称 Chain of Responsibility
结构
意图 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
适用性
  • 有多个的对象可以处理一个请求,哪个对象处理该请求运行时刻自动确定。
  • 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • 可处理一个请求的对象集合应被动态指定。




名称 Command
结构
意图 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
适用性
  • 像上面讨论的M e n u I t e m 对象那样,抽象出待执行的动作以参数化某对象。你可用过程语言中的回调(c a l l b a c k )函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。C o m m a n d 模式是回调机制的一个面向对象的替代品。
  • 在不同的时刻指定、排列和执行请求。一个C o m m a n d 对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
  • 支持取消操作。C o m m a n d 的E x c u t e 操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。C o m m a n d 接口必须添加一个U n e x e c u t e 操作,该操作取消上一次E x e c u t e 调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用U n e x e c u t e 和E x e c u t e 来实现重数不限的“取消”和“重做”。
  • 支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在C o m m a n d 接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用E x e c u t e 操作重新执行它们。
  • 用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务( t r a n s a c t i o n )的信息系统中很常见。一个事务封装了对数据的一组变动。C o m m a n d 模式提供了对事务进行建模的方法。C o m m a n d 有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。




名称 Interpreter
结构
意图 给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
适用性
  • 当有一个语言需要解释执行, 并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。而当存在以下情况时该模式效果最好:
  • 该文法简单对于复杂的文法, 文法的类层次变得庞大而无法管理。此时语法分析程序生成器这样的工具是更好的选择。它们无需构建抽象语法树即可解释表达式, 这样可以节省空间而且还可能节省时间。
  • 效率不是一个关键问题最高效的解释器通常不是通过直接解释语法分析树实现的, 而是首先将它们转换成另一种形式。例如,正则表达式通常被转换成状态机。但即使在这种情况下, 转换器仍可用解释器模式实现, 该模式仍是有用的。




名称 Iterator
结构
意图 提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
适用性
  • 访问一个聚合对象的内容而无需暴露它的内部表示。
  • 支持对聚合对象的多种遍历。
  • 为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。




名称 Mediator
结构
意图 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
适用性
  • 一组对象以定义良好但是复杂的方式进行通信。产生的相互依赖关系结构混乱且难以理解。
  • 一个对象引用其他很多对象并且直接与这些对象通信,导致难以复用该对象。
  • 想定制一个分布在多个类中的行为,而又不想生成太多的子类。




名称 Memento
结构
意图 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
适用性
  • 必须保存一个对象在某一个时刻的(部分)状态, 这样以后需要时它才能恢复到先前的状态。
  • 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。




名称 Observer
结构
意图 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
适用性
  • 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
  • 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
  • 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。




名称 State
结构
意图 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
适用性
  • 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
  • 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常, 有多个操作包含这一相同的条件结构。S t a t e模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。




名称 Strategy
结构
意图 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
适用性
  • 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
  • 需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法。当这些变体实现为一个算法的类层次时[ H O 8 7 ] ,可以使用策略模式。
  • 算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
  • 一个类定义了多种行为, 并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的S t r a t e g y 类中以代替这些条件语句。




名称 Template Method
结构
意图 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Te m p l a t e M e t h o d 使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
适用性
  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
  • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。这是O p d y k e 和J o h n s o n 所描述过的“重分解以一般化”的一个很好的例子[ O J 9 3 ]。首先识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • 控制子类扩展。模板方法只在特定点调用“h o o k ”操作(参见效果一节),这样就只允许在这些点进行扩展。




名称 Visitor
结构
意图 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
适用性
  • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Vi s i t o r 使得你可以将相关的操作集中起来定义在一个类中。当该对象结构被很多应用共享时,用Vi s i t o r 模式让每个应用仅包含需要用到的操作。
  • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。


欢迎交流:zj_mail@chinese.com
2006年04月17日

题目:先输入一个自然数n(n≤3000000),然后对此自然数按照如下方法进行处理

1·不作任何处理:

2·在它的左边加上一个自然数,但该自然数不能超过原数的一半;

3·加上数后,继续按此规则进行处理,直到不能再而 自然数为止。

例如n=6
6
16
26
126
36
136

所以满足要求的个数为6。

Input

包含多个测试数据,每行是一个整数n(1<=n<=3000000)

Output

一个整数,表示解的个数(保证不超过50位)

Sample Input

6

Sample Output

6

Source

解:
//这里我给出0~9自然数的算法,其实和0~3000等的基本一样了
//由于需要锻炼C++的东西,所以我的算法一般都以C++的面向对象和STL为基础写的
#include <iostream>
#include <list>
#include <vector>
using namespace std;


class A
{
private:
    vector<int> it;
    vector<int> ij;
public:
   
    void Display()
    {
        vector<int>::iterator i;
        for (i=it.begin(); i!=it.end(); i++)
        {
            cout<<*i<<endl;
        }
    }
   
    int retCount()
    {
        return it.size();
    }

    void Add()
    {
        int k = 1;
        int p=0;
        for (int i=0; i<ij.size(); i++)
        {
            p += ij[i] * k;
            k *= 10;
           
        }
        it.push_back(p);
    }
   

    void go(int value)
    {       
        ij.push_back(value);
        Add();

        int i = value/2;
        if(i == 0)
        {   
            ij.pop_back();       
            return;
        }

        for (int j=i; j!=0; j–)
        {

            go(j);
           
        }
        ij.pop_back();
    }
};

int main()
{
    int inValue;
    cin>>inValue;
    if (inValue<=0 || inValue>=10)
    {
        cout<<"error!"<<endl;
        return 0;
    }
    A a;
    a.go(inValue);
   
    cout<<endl<< a.retCount()<<endl<<endl;
    a.Display();
    cin.get();
    cin.get();
    return 0;
}

1.

名称:  Factory Method

结构图:

意图:  定义一个用于创建对象的接口,让子类决定实例化哪一个类.Factory Mehtod使一个类的实例化延迟到其子类.

适用性: 

1)当一个类不知道它所必须创建的对像的类时.

2)当一个类希望由它的子类来指定它所创建的对象的时候.

3)当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候.

2.

名称:  abstract Factory

结构图:

意图:  提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类.

适用性: 

1)一个系统要独立于它的产品的创建\组合以及表示时.

2)一个系统要由多个产品系统中的一个来配置时.

3)当你要强调一系列相关的产品对象的设计以便进行联合使用时.

4)当你提供一个产品类库,而是想显示它们的接口而不是实现时.

3.

名称:  Builder

结构图:

意图:  当一个复杂对象的构建与它的表现分离,使得同样的构建过程可以创建不同的表示.

适用性:

1)当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时.

2)当构造过程必须允许被构造的对象有不同的表示时.

4.

名称:  Protetype

结构图:

意图:  用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.

适用性:

1)当要实例化的类是在运行时指定时,例如:通过动态装载,或者

2)为了避免创建一个与产品类层次平行的工厂类层次时;或者

3)当一个类的实例只能有几个不同状态组合中的一种时,建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些.

5.

名称:  Singleton

结构图:

意图:  保证类仅有一个实例.并提供一个访问它的全局访问点.

适用性:

1)当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时.

2)当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个扩展的实例时.

6.

名称:  Adapter

结构图:

意图:  将一个类的接口转换成客户希望的另一个接口.Adapter模式使得原来由于接口不兼容而不能一起工作的那些类可以一起工作.

适用性:

1)你想使用一些已经存在的类,而它的接口不符合你的需求.

2)你想创建一个可复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)协同工作.

3)(仅适用于对象Adapter)你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口.对象适配器可以适配它的父类接口.

7.

名称:  Bridge

结构图:

意图:  将抽象部分与它的实现部分分离,使它们都可以独立地变化.

适用性:

1)你不希望在抽象和它的实现部分之间有一个固定的绑定关系.例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换.

2)类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充.这时Bridge模式使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充.

3)对一个抽象的实现部分的修改应对客户不产生影响,即客户的代码不必重新编译.

4)(C++)你想对客户完全隐藏抽象的实现部分.在C++中,类的表示在类接口中是可见的.

5)有许多类要生成.这样一种类层次结构说明你必须将一个对象分解成另个部分.

6)你想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点.一个简单的例子就是String类,在这个类多个对象可以共享一个字符串表示(StringRep).

8.

名称:  Composite

结构图:

意图:  将对象组合成树型结构以表示"部分-整体"的层次结构.Composite使得用户对单个对象和组合对象的使用具有一致性.

适用性:

1)你想表示对象的部分-整体层次结构.

2)你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象.

9.

名称:  Decorator

结构图:

意图:动态地给一个对象添加一些额外的职责.就增加功能来说,Decorator模式相比生成子类更为灵活.

适用性:

1)在不影响其他对象的情况下,以动态\透明的方式给单个对象添加职责.

2)处理那些可以撤消的职责.

3)当不能采用生成子类的方法进行扩充时,一种情况时,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类的数据呈爆炸性增长;另一种情况是因为类定义被隐藏,或类定义不能用于生成子类.

10.

名称:    Facade

结构图:

意图:  为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用.

适用性:

1)当你要为一个复杂子系统提供一个简单接口时.子系统往往因为不断演化而越来越复杂.大多数模式使用时都会产生更多更小的类.这使得子系统更具有可重用性,也更容易对子系统进行定制,但这也给那些不需要定制子系统的用户带来一些使用上的困难.Facade可以提供一个简单的缺省视图,这一视图对大多数用户来说已经足够,而那些需要更多的可定制性的用户可以越过Facade层.

2)客户程序与抽象类的实现部分之间存在很大的依赖性.引如Facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性.

3)当你需要构建一个层次结构的子系统时,使用Facade模式定义子系统中每层的入口点.如果子系统之间是相互依赖的,你可以让它们仅通过Facade进行通讯,从而简化它们之间的依赖关系.

11.

名称:  Flyweight

结构图:

意图:  运用共享技术有效地支持大量细粒度的对象.

适用性:

1)一个应用程序使用大量的对象.

2)完全由于使用大量的对象,造成很大的存储开销.

3)对象的大多数状态都可变为外部状态.

4)如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象.

5)应用程序不依赖与对象标志.由于Flyweight对象可以被共享,对于概念上明显有别的对象,标志测试将返回真值.

12.

名称:    Proxy

结构图:

意图:    为其他对象提供一种代理以控制对这个对象的访问.

适用性:

1)在需要用比较通用和复杂的对象指针代替简单的指针的时候,使用P r o x y 模式。下面是一 些可以使用P r o x y 模式常见情况:
1> 远程代理(Remote Proxy )为一个对象在不同的地址空间提供局部代表。 NEXTSTEP[Add94] 使用N X P r o x y 类实现了这一目的。Coplien[Cop92] 称这种代理为“大使” (A m b a s s a d o r )。
2 >虚代理(Virtual Proxy )根据需要创建开销很大的对象。在动机一节描述的I m a g e P r o x y 就是这样一种代理的例子。
3>保护代理(Protection Proxy )控制对原始对象的访问。保护代理用于对象应该有不同 的访问权限的时候。例如,在C h o i c e s 操作系统[ C I R M 9 3 ]中K e m e l P r o x i e s 为操作系统对象提供 了访问保护。
4>智能指引(Smart Reference )取代了简单的指针,它在访问对象时执行一些附加操作。 它的典型用途包括:

2)对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它(也称为S m a r tP o i n t e r s[ E d e 9 2 ] )。

3)当第一次引用一个持久对象时,将它装入内存。

4)在访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它。

2006年04月14日

1)GridView绑定数据源控件,需要有编辑和删除选项按钮时,数据源控件必须提供SQ操作L语句或存储过程调用,一般,我的推荐做法是,使用无意义的SQL语句或存储过程来使GridView的编辑和删除按钮可以生成,具体的编辑更新和删除操作在代码运行时而不是在控件设计时指定,虽然多写了一点代码,但是对以后的扩展应用是很有好处的,建议所有的数据操作,不管是简单还是复杂的,都使用三层结构,这样从WEB到WIN之间的软件转换将十分轻松.而且,统一使用三层结构,虽然代码量会多一点,但是可控制性是相当好的,如果组织合理应用统一,这样的做法也是相当高效的.


2)可以指定GridView绑定列的ReadOnly属性为false,这样在编辑生效时,将不会将此列自动设置为textbox形式的可编辑控件.

3)使用GridView控件中的BoundField子控件可以绑定一个数据表的某个字段列,只要将BoundField控件的DataField属性设置为要绑定的数据表的字段名,如果是存储过程select返回的比如@A,那么就将DataField设置成A;并且,可以使用DataFormatString属性来格式化字段输出,注意,同时要将HtmlEncode属性设置为false;HeaderText属性则是GridView表现出来的字段名称,一般将其设置为中文字段名;要设置一些其他的样式属性,比如左对齐,背景色等,通过"样式"这个属性来选择设置;另外,如果绑定到字段的数据为空时显示的默认值,则可以通过设置NullDisplayText字段来显示.

4)要设置GridView的主键,可以在其DataKeyNames属性中设置.一般,如果数据源返回5个字段,如果想自动生成列,可使AutoGenerateColumns为true,如果要有选择的自己设置要显示的列或需要对显示的效果做设置,则先使AutoGenerateColumns为false,然后在GridView的编辑列选项操作中,自己使用BoundField控件的DataField属性设置为要绑定的数据表的字段名,并做相应的显示设置即可.

5)GridView有一些常用的事件,我在使用中经常用到的是:RowCommand(只要GridView控件中有按钮按下就会激发,并可找到是GridView的当前第几行),RowDeleting(删除前,做一些判断或准备工作,我建议直接在此处调用中间业务层的数据操纵服务方法,如果判断结果是删除操作不能进行,则可以设置e.cancel=true,则不会执行数据源控件中指定的SQL语句或存储过程),.RowDeleted(删除后,做一些善后工作),RowUpdating(更新前),RowUpdated(更新后),RowEditing(按编辑按钮时触发),Sorting(排序前),Sorted(排序后),SelectedIndexChanging(选择新行时,还没有做任何新动作前激发),SelectedIndexChanged(选择新行,并执行操作时激发). 需要注重说明的是,所有操作"前"激发的事件,都有"ing"后缀名,这里一般进行一些判断工作,判断操作的条件是否满足,如果满足,则其后执行设计时数据源控件的SQL语句或存储过程,但是,如果条件不满足,则可以使e.Cancel = true,这时将不会继续执行SQL语句和存储过程.

6)一般,ASP.NET控件都有一个专门处理客户端脚本的属性,此属性关联一些javaScript等的脚本代码,这些客户端脚本代码将最优先执行,来进行一些选择或判断,比如:javascript:event.returnValue = window.confirm("你真的想要删除该销售单吗?"),返回true,将正常运行此控件绑定的事件处理程序,如果返回false,则就好象没有点击此控件一样,并不会向下执行服务端代码.

7)GridView内部的一些按钮型控件(ButtonField),点击时触发的处理事件是相同的,可以通过设置CommandName属性的不同字符串来加以区分,如,在RowCommand事件中,可以通过e.CommandName属性来获取点击的特定ButtonField的CommandName是否与指定的字符串相同,从而进入不同的流程处理.

8)GridView的DataSource属性绑定到集合类型上,如:数组,泛型类,DataSet,DataTable等.而它的DataSourceID属性则绑定到特定的数据源控件上,如SqlDataSource,ObjectDataSource,XmlDataSource等.然后,调用DataBind()方法具体绑定数据,DataBind()前只是指定数据来源.

9)一个小技巧:当GridView绑定一个对象集合时,如果该对象的公开属性是英文,则GridView的列名也是英文,如果该对象的公开属性是中文,则GridView的列名也是中文,这个技巧可以不同设置GridView的相关属性而得到中文字段显示的GridView报表.在一些要求特殊的场合还是一个不错的解决方法.

10)

2006年04月07日

                    由C++转向C#:我们需要注意哪些方面的变化?
每隔10年左右,编程人员就需要花费大量的时间和精力去学习新的编程技术。在80年代是Unix和C,90年代是Windows和C++,现在又轮到了微软的.NETFramework和C#。尽管需要学习新的技术,但由此带来的好处却远高于付出的劳动。幸运的是,使用C#和.NET进行的大多数工程的分析和设计与在C++和Windows中没有本质的变化。在本篇文章中,我将介绍如何实现由C++到C#的飞跃。

已经有许多文章介绍过C#对C++的改进,在这里我就不再重复这些问题了。在这里,我将重点讨论由C++转向C#时最大的变化:由不可管理的环境向可管理的环境的变化。此外,我还会提出一些C#编程人员容易犯的错误供大家参考,此外,还将说明一些C#语言的能够影响编程的新功能。

转向可管理的环境

C++的设计目标是低级的、与平台无关的面向对象编程语言,C#则是一种高级的面向组件的编程语言。向可管理环境的转变意味着你编程方式思考的重大转变,C#不再处理细微的控制,而是让架构帮助你处理这些重要的问题。例如,在C++中,我们就可以使用new在栈中、堆中、甚至是内存中的某一特定位置创建一个对象。

在.NET的可管理环境中,我们再不用进行那样细微的控制了。在选择了要创建的类型后,它的位置就是固定的了。简单类型(ints、double和long)的对象总是被创建在栈中(除非它们是被包含在其他的对象中),类总是被创建在堆中。我们无法控制对象是创建在堆中哪个位置的,也没有办法得到这个地址,不能将对象放置在内存中的某一特定位置。(当然也有突破这些限制的方法,但那是很另类的方法。)我们再也不能控制对象的生存周期,C#没有destructor。碎片收集程序会将对象所占用的内存进行回收,但这是非显性地进行的。

正是C#的这种结构反映了其基础架构,其中没有多重继承和模板,因为在一个可管理的碎片收集环境中,多重继承是很难高效地实现的。

C#中的简单类型仅仅是对通用语言运行库(CLR)中类型的简单映射,例如,C#中的int是对System.Int32的映射。C#中的数据类型不是由语言本身决定的,而是由CLR决定的。事实上,如果仍然想在C#中使用在VisualBasic中创建的对象,就必须使自己的编程习惯更符合CLR的规定。

另一方面,可管理的环境和CLR也给我们带来了好处。除了碎片收集和所有.NET语言中统一的数据类型外,它还提供给我们一个功能强大的面向组件的编程语言,无须对后期绑定提供特别的支持,类型发现和后期绑定都是被内置在语言中的。属性是C#语言中的第一类的成员,事件和代理也是。

可管理环境最主要的优点是.NETFramework。尽管在所有的.NET语文中都可以使用这种框架,但C#可以更好地使用.NET框架中丰富的类、接口和对象。

Traps

C#看起来与C++非常相似,这使得我们在由C++转向C#时比较轻松,但其中也有一些容易出错的地方。在C++中编写得非常漂亮的代码,在C#中会不能通过编译,甚至会出现意想不到的结果。C#与C++之间在语法上的变化并不大,编译器能够发现这二者之间大部分的差异,我在这里就不再多费笔墨了,在这里我介绍几个容易出问题的比较重要的变化:

引用类型和值类型

在C#中,值类型和引用类型数据是有区别的。简单类型(int、long、double等)和结构属于值类型数据,类和对象属于引用类型数据。除非是包含在引用类型的变量中,与在C++中一样,值类型变量的值存储在栈中。引用类型的变量也存储在栈中,但它的值是一个存储在堆中的对象的地址,这一点也与C++类似。值类型变量是将自己的值传递给方法,而引用类型变量则将自己的指针传递给方法。

结构

C#中的结构与C++中有非常明显的区别。在C++中,结构更象是类,除了缺省的继承外,其缺省的访问权限是public而不是private。在C#中,结构与类截然不同,它是用来封装轻型对象的,是值类型的数据类型,在传递时传送的是变量的值,而不是其地址。此外,它们也有一些不适用于类的限制,例如,它是不能继承的,也没有除System.ValueType之外的基本类。结构还不能定义一个缺省的constructor。

另一方面,由于结构比类的效率要高,因此它非常适合于创建轻型对象。因此,如果它的缺点对你的软件没有影响,使用结构比使用类效率要高得多,尤其是对于小对象而言。

所有的一切都是对象

在C#中,所有的东西都是由继承Object得到的,包括创建的类和int、structs等值类型的变量。Object类提供了一些有用的方法,例如ToString,使用ToString的一个例子是与System.Console.WriteLine一起使用,它可以接受一个字符串和许多对象。与使用printf语句不同,要使用WriteLine,需要提供代换变量。假设myEmployee是用户定义的Employee类的一个实例,myCounter是用户定义的Counter类的一个实例:
 
Console.WriteLine("Theemployee:{0},thecountervalue:{1}",
myEmployee,myCounter);    

其中的WriteLine会调用每个对象的Object.ToString方法,替换作为参数返回的变量。如果Employee类不覆盖ToString,就会调用缺省的实现(由System.Object继承得到的),它将把类的名字作为一个字符串返回。Counter会覆盖ToString,返回一个整型的变量,因此,上面代码的输出为:
 
Theemployee:Employee,thecountervalue:12    

如果向WriteLine传递一个整型变量会发生什么情况呢?由于不能对整型变量调用ToString,编译器将自动将整型变量封装在一个对象的实例中。当WriteLine调用ToString时,对象就会返回表示整型变量值的字符串。下面的代码就说明了这个问题:

类的使用
 
usingSystem;
//不覆盖ToString的类
publicclassEmployee
{
}
//覆盖了ToString的类
publicclassCounter
{
privateinttheVal;
publicCounter(inttheVal)
{
this.theVal=theVal;
}
publicoverridestringToString()
{
Console.WriteLine("CallingCounter.ToString()");
returntheVal.ToString();
}
}
publicclassTester
{
publicstaticvoidMain()
{
//创建类的实例
Testert=newTester();
//调用非静态成员
//(mustbethroughaninstance)
t.Run();
}
//演示调用ToString的非静态方法
publicvoidRun()
{
EmployeemyEmployee=newEmployee();
CountermyCounter=newCounter(12);
Console.WriteLine("Theemployee:{0},thecountervalue:{1}",
myEmployee,myCounter);
intmyInt=5;
Console.WriteLine("Herearetwointegers:{0}and{1}",17,myInt);
}
}    



引用型参数和输出型参数


与C++中相同,C#中的方法也只能有一个返回值。在C++中,我们通过将指针或索引作为参数而克服了这个限制,被调用的方法改变其中的参数,调用方法就可以得到新的值了。

向方法中传递一个索引作为参数时,只能严格地按传递索引或指针所能够提供的方式访问原来的对象。对于值类型变量而言,就不能采用这种方法了。如果要通过引用型参数传递值型变量,就需要在其前面加上ref关健字。如下所示:
 
publicvoidGetStats(refintage,refintID,refintyearsServed)    

需要注意的是,既需要在方法的定义中使用ref关健字,也需要在对方法的实际调用中使用ref关健字。
 
Fred.GetStats(refage,refID,refyearsServed);    

现在,我们可以在调用方法中定义age、ID和yearsServed变量,并将它们传递给GetStats,得到改变后的值。

C#要求明确的赋值,也就是说,在调用GetStats方法之前,必须对age、ID和yearsServed这三个局部变量进行初始化,这一工作似乎有点多余,因为我们仅仅使用它们从GetStats中得到新的变量的值。为了解决这一问题,C#提供了out关健字,表示我们可以向方法中传递没有被初始化的变量,这些变量将通过引用变量的方式进行传递:
 
publicvoidGetStats(outintage,outintID,outintyearsServed)    

当然了,调用方法也必须作出相应的变化:
 
Fred.GetStats(outage,outID,outyearsServed);    

New的调用

在C++中,new关健字可以在堆上生成一个对象。在C#中却不是这样。对于引用类型变量而言,new关健字在堆上生成一个对象;对于结构等值类型变量而言,new关健字在栈中生成一个对象,并需要调用constructor。

事实上,我们可以不使用new关健字而在栈上生成一个结构类型的变量,但这时需要注意的是,New关健字能够初始化对象。如果不使用new,则在使用前必须手工地对结构中的所有成员进行初始化,否则在编译时会出错。

对象的初始化
 
usingSystem;//有二个成员变量和一个构造器的简单结构
publicstructPoint
{
publicPoint(intx,inty)
{
this.x=x;
this.y=y;
}

publicintx;
publicinty;
}

publicclassTester
{
publicstaticvoidMain()
{
Testert=newTester();
t.Run();
}

publicvoidRun()
{
Pointp1=newPoint(5,12);
SomeMethod(p1);//fine

Pointp2;//不调用new而直接创建

//编译器编译到这里时会出错,因为p2的成员变量没有被初始化
//SomeMethod(p2);

//手工对它们进行初始化
p2.x=1;
p2.y=2;

SomeMethod(p2);

}

//一个可以接受Point作为参数的方法
privatevoidSomeMethod(Pointp)
{
Console.WriteLine("Pointat{0}x{1}",
p.x,p.y);
}
}    

属性

大多数的C++编程人员都希望使成员变量的属性为private,这种隐藏数据的想法促进了数据封装概念的出现,使我们能够在不改变用户依赖的接口的情况下而改变类的实现。通常情况下,我们只希望客户获取或设置这些成员变量的值。因此,C++编程人员开发出了用来存取private成员变量的存取器。

在C#中,属性是类的第一级成员。对于客户而言,属性看起来象一个成员变量。对于类的实现者而言,它看起来更象是方法。这种设计很巧妙,既可以实现数据的隐藏和封装,又可以使客户很方便地访问成员变量。

我们可以在Employee类中添加一个Age属性,使客户可以很方便地获取和设置员工年龄这个类的成员:
 
publicintAge
{
get
{
returnage;
}
set
{
age=value;
}
}    

关健字value可以被属性隐性地使用。如果编写如下的代码:
 
Fred.Age=17;    

编译器将会把值17传递给value。

通过只采用Get而不采用Set,我们可以为YearsServed创建一个只读的属性:
 
publicintYearsServed
{
get
{
returnyearsServed;
}
}Accessors的使用
privatevoidRun()
{
EmployeeFred=newEmployee(25,101,7);
Console.WriteLine("Fred’sage:{0}",
Fred.Age);
Fred.Age=55;
Console.WriteLine("Fred’sage:{0}",
Fred.Age);

Console.WriteLine("Fred’sservice:{0}",
Fred.YearsServed);
//Fred.YearsServed=12;//是不被允许的

}    

我们可以通过属性获取Fred的年龄,也可以使用这一属性设置年龄。我们虽然可以访问YearsServed属性获得它的值,但不能设置值。如果没有注释掉最后一行的代码,在编译时就会出错。

如果以后决定从数据库中获取Employee的年龄,我们就只需要改变存取器的实现,而客户不会受到任何影响。

数组
C#提供了一个数组类,它比C/C++中传统的数组更智能化。例如,在C#中写数组时不会超出边界。此外,数组还有一个更智能的伙伴—ArrayList,可以动态地增长,管理对数组大小不断变化的需求。

C#中的数组有三种形式:一维数组、多维均匀数组(象C++中传统的数组那样)、非均匀数组(数组的数组)。我们可以通过下面的代码创建一维数组:
 
int[]myIntArray=newint[5];    

另外,还可以以如下的方式对它进行初始化:
 
int[]myIntArray={2,4,6,8,10};    

我们可以通过如下方式创建一个43的均匀数组:
 
int[,]myRectangularArray=newint[rows,columns];    

我们可以按如下方式对该数组进行初始化:
 
int[,]myRectangularArray=
{
{0,1,2},{3,4,5},{6,7,8},{9,10,11}
};    

由于非均匀数组是数组的数组,因此,我们只能创建一维非均匀数组:
 
int[][]myJaggedArray=newint[4][];    

然后再创建内部的每个数组:
 
myJaggedArray[0]=newint[5];
myJaggedArray[1]=newint[2];
myJaggedArray[2]=newint[3];
myJaggedArray[3]=newint[5];    

由于数组是由继承System.Array对象而得到的,因此,它们带有许多包括Sort、Reverse在内的许多有用的方法。

索引器

我们可以创建象数组一样的对象。例如,我们可以创建一个显示一系列字符串的列表框,可以把列表框当作一个数组,使用一个索引就可以很方便地访问列表框中的内容。
 
stringtheFirstString=myListBox[0];
stringtheLastString=myListBox[Length-1];    

这是通过索引器完成的。索引器在很大程度上象一个属性,但支持索引操作的语法。图4显示了一个后面跟着索引操作符的属性,图5显示如何完成一个很简单的ListBox类并对它进行索引:

界面

软件界面是二种对象之间如何进行交互的契约。如果一个对象发布了一个界面,就等于向所有可能的客户声明:我支持下面的方法、属性、事件和索引器。

C#是一种面向对象的语言,因此这些契约被封装在一个被称作界面的实体中,界面定义了封装着契约的引用型类型的对象。从概念上来讲,界面与抽象类非常相似,二者的区别是抽象类可以作为一系列衍生类的基础类,界面则是与其他继承树结合在一起的。

IEnumerable界面

再回到上面的例子中。象在普通的数组中那样,使用foreach-loop循环结构就能够很好地打印ListBoxTest类中的字符串,通过在类中实现IEnumerable界面就能实现,这是由foreach-loop循环结构隐性地完成的。在任何支持枚举和foreach-loop循环的类中都可以实现IEnumerable界面。

IEnumerable界面只有一个方法GetEnumerator,其任务是返回一个特别的IEnumerator的实现。从语法的角度来看,Enumerable类能够提供一个IEnumerator。
 
Figure5ListBoxClass
usingSystem;
//简化的ListBox控制
publicclassListBoxTest
{
//用字符串初始化该ListBox
publicListBoxTest(paramsstring[]initialStrings)
{
//为字符串分配空间
myStrings=newString[256];
//把字符串拷贝到构造器中
foreach(stringsininitialStrings)
{
myStrings[myCtr++]=s;
}
}
//在ListBox的末尾添加一个字符串
publicvoidAdd(stringtheString)
{
myStrings[myCtr++]=theString;
}
publicstringthis[intindex]
{
get
{
if(index<0index>=myStrings.Length)
{
//处理有问题的索引
}
returnmyStrings[index];
}
set
{
myStrings[index]=value;
}
}
//返回有多少个字符串
publicintGetNumEntries()
{
returnmyCtr;
}
privatestring[]myStrings;
privateintmyCtr=0;
}
publicclassTester
{
staticvoidMain()
{
//创建一个新的列表并初始化
ListBoxTestlbt=newListBoxTest("Hello","World");
//添加一些新字符串
lbt.Add("Who");
lbt.Add("Is");
lbt.Add("John");
lbt.Add("Galt");
stringsubst="Universe";
lbt[1]=subst;
//访问所有的字符串
for(inti=0;i<lbt.GetNumEntries();i++)
{
Console.WriteLine("lbt[{0}]:{1}",i,lbt[i]);
}
}
}    

Enumerator必须实现IEnumerator方法,这可以直接通过一个容器类或一个独立的类实现,后一种方法经常被选用,因为它可以将这一任务封装在Enumerator类中,而不会使容器类显得很混乱。我们将在上面代码中的ListBoxTest中添加Enumerator类,由于Enumerator类是针对我们的容器类的(因为ListBoxEnumerator必须清楚ListBoxTest的许多情况),我们将使它在ListBoxTest中成为不公开的。在本例中,ListBoxTest被定义来完成IEnumerable界面,IEnumerable界面必须返回一个Enumerator。
 
publicIEnumeratorGetEnumerator()
{
return(IEnumerator)newListBoxEnumerator(this);
}    

注意,方法将当前的ListBoxTest对象(this)传递给Enumerator,这将使Enumerator枚举这一指定的ListBoxTest对象中的元素。

实现这一类的Enumerator在这里被实现为ListBoxEnumerator,它在ListBoxTest中被定义成一个私有类,这一工作是相当简单的。

被枚举的ListBoxTest作为一个参数被传递给constructor,ListBoxTest被赋给变量myLBT,构造器还会将成员变量index设置为-1,表明对象的枚举还没有开始。
 
publicListBoxEnumerator(ListBoxTesttheLB)
{
myLBT=theLB;
index=-1;
}    

MoveNext方法对index进行加1的操作,然后确保没有超过枚举的对象的边界。如果超过边界了,就会返回false值,否则返回true值。
 
publicboolMoveNext()
{
index++;
if(index>=myLBT.myStrings.Length)
returnfalse;
else
returntrue;
}    

Reset的作用仅仅是将index的值设置为-1。

Current返回最近添加的字符串,这是一个任意的设定,在其他类中,Current可以有设计人员确定的意义。无论是如何设计的,每个进行枚举的方法必须能够返回当前的成员。
 
publicobjectCurrent
{
get
{
return(myLBT[index]);
}
}    

对foreach循环结构的调用能够获取枚举的方法,并用它处理数组中的每个成员。由于foreach循环结构将显示每一个字符串,而无论我们是否添加了一个有意义的值,我们将myStrings的初始化改为8个条目,以保证显示的易于处理。
 
myStrings=newString[8];    

使用基本类库

为了更好地理解C#与C++的区别和解决问题方式的变化,我们先来看一个比较简单的例子。我们将创建一个读取文本文件的类,并在屏幕上显示其内容。我将把它做成多线程程序,以便在从磁盘上读取数据时还可以做其他的工作。

在C++中,我们可能会创建一个读文件的线程和另一个做其他工作的线程,这二个线程将各自独立地运行,但可能会需要对它们进行同步。在C#中,我们也可以完成同样的工作,由于.NET框架提供了功能强大的异步I/O机制,在编写线程时,我们会节省不少的时间。

异步I/O支持是内置在CLR中的,而且几乎与使用正常的I/O流类一样简单。在程序的开始,我们首先通知编译器,我们将在程序中使用许多名字空间中的对象:
 
usingSystem;
usingSystem.IO;
usingSystem.Text;    

在程序中包含System,并不会自动地包含其所有的子名字空间,必须使用using关健字明确地包含每个子名字空间。我们在例子中会用到I/O流类,因此需要包含System.IO名字空间,我们还需要System.Text名字空间支持字节流的ASCII编码。

由于.NET架构为完成了大部分的工作,编写这一程序所需的步骤相当简单。我们将用到Stream类的BeginRead方法,它提供异步I/O功能,将数据读入到一个缓冲区中,当缓冲区可以处理时调用相应的处理程序。

我们需要使用一个字节数组作为缓冲区和回叫方法的代理,并将这二者定义为驱动程序类的private成员变量。
 
publicclassAsynchIOTester
{
privateStreaminputStream;
privatebyte[]buffer;
privateAsyncCallbackmyCallBack;    

inputStream是一个Stream类型的变量,我们将对它调用BeginRead方法。代理与成员函数的指针非常相似。代理是C#的第一类元素。

当缓冲区被磁盘上的文件填满时,.NET将调用被代理的方法对数据进行处理。在等待读取数据期间,我们可以让计算机完成其他的工作。(在本例中是将1个整型变量由1增加到50000,但在实际的应用程序中,我们可以让计算机与用户进行交互或作其他有意义的工作。)

本例中的代理被定义为AsyncCallback类型的过程,这是Stream的BeginRead方法所需要的。System空间中AsyncCallback类型代理的定义如下所示:
 
publicdelegatevoidAsyncCallback(IAsyncResultar);    

这一代理可以是与任何返回void类型值、将IAsyncResult界面作为参数的方法相关联的。在该方法被调用时,CLR可以在运行时传递IAsyncResult界面对象作为参数。我们需要如下所示的形式定义该方法:
 
voidOnCompletedRead(IAsyncResultasyncResult)    

然后在构造器中与代理连接起来:
 
AsynchIOTester()
{
???
myCallBack=newAsyncCallback(this.OnCompletedRead);
}    

上面的代码将代理的实例赋给成员变量myCallback。下面是全部程序的详细工作原理。在Main函数中,创建了一个类的实例,并让它开始运行:
 
publicstaticvoidMain()
{
AsynchIOTestertheApp=newAsynchIOTester();
theApp.Run();
}    

new关健字能够启动构造器。在构造器中我们打开一个文件,并得到一个Stream对象。然后在缓冲中分配空间并与回调机制联结起来。
 
AsynchIOTester()
{
inputStream=File.OpenRead(@"C:\MSDN\fromCppToCS.txt");
buffer=newbyte[BUFFER_SIZE];
myCallBack=newAsyncCallback(this.OnCompletedRead);
}    

在Run方法中,我们调用了BeginRead,它将以异步的方式读取文件。
 
inputStream.BeginRead(
buffer,//存放结果
0,//偏移量
buffer.Length,//缓冲区中有多少字节
myCallBack,//回调代理
null);//本地对象    

这时,我们可以完成其他的工作。
 
for(longi=0;i<50000;i++)
{
if(i%1000==0)
{
Console.WriteLine("i:{0}",i);
}
}    

文件读取操作结束后,CLR将调用回调方法。
 
voidOnCompletedRead(IAsyncResultasyncResult)
{    

在OnCompletedRead中要做的第一件事就是通过调用Stream对象的EndRead方法找出读取了多少字节:
 
intbytesRead=inputStream.EndRead(asyncResult);    

对EndRead的调用将返回读取的字节数。如果返回的数字比0大,则将缓冲区转换为一个字符串,然后将它写到控制台上,然后再次调用BeginRead,开始另一次异步读的过程。
 
if(bytesRead>0)
{
Strings=Encoding.ASCII.GetString(buffer,0,bytesRead);
Console.WriteLine(s);
inputStream.BeginRead(buffer,0,buffer.Length,
myCallBack,null);
}    

现在,在读取文件的过程中就可以作别的工作了(在本例中是从1数到50000),但我们可以在每次缓冲区满了时对读取的数据进行处理(在本例中是向控制台输出缓冲区中的数据)。有兴趣的读者可以点击此处下载完整的源代码。

异步I/O的管理完全是由CLR提供的,这样,在网络上读取文件时,会更好些。

在网络上读取文件

在C++中,在网络上读取文件需要有相当的编程技巧,.NET对此提供了广泛的支持。事实上,在网络上读取文件仅仅是基础类库中Stream类的另一种应用。

首先,为了对TCP/IP端口(在本例中是65000)进行监听,我们需要创建一个TCPListener类的实例。
 
TCPListenertcpListener=newTCPListener(65000);    

一旦创建后,就让它开始进行监听。
 
tcpListener.Start();    

现在就要等待客户连接的要求了。
 
SocketsocketForClient=tcpListener.Accept();    

TCPListener对象的Accept方法返回一个Socket对象,Accept是一个同步的方法,除非接收到一个连接请求它才会返回。如果连接成功,就可以开始向客户发送文件了。
 
if(socketForClient.Connected)
{
???    

接下来,我们需要创建一个NetworkStream类,将报路传递给constructor:
 
NetworkStreamnetworkStream=newNetworkStream(socketForClient);    

然后创建一个StreamWriter对象,只是这次不是在文件上而是在刚才创建的NetworkStream类上创建该对象:
 
System.IO.StreamWriterstreamWriter=
newSystem.IO.StreamWriter(networkStream);    

当向该流写内容时,流就通过网络被传输给客户端。

客户端的创建

客户端软件就是一个TCPClient类的具体例子,TCPClient类代表连向主机的一个TCP/IP连接。
 
TCPClientsocketForServer;
socketForServer=newTCPClient("localHost",65000);    

有了TCPClient对象后,我们就可以创建NetworkStream对象了,然后在其上创建StreamReader类:
 
NetworkStreamnetworkStream=socketForServer.GetStream();
System.IO.StreamReaderstreamReader=
newSystem.IO.StreamReader(networkStream);    

现在,只要其中有数据就读取该流,并将结果输出到控制台上。
 
do
{
outputString=streamReader.ReadLine();

if(outputString!=null)
{
Console.WriteLine(outputString);
}
}
while(outputString!=null);    

为了对这一段代码进行测试,可以创建如下一个测试用的文件:
 
Thisislineone
Thisislinetwo
Thisislinethree
Thisislinefour    

这是来自服务器的输出:
 
Output(Server)
Clientconnected
SendingThisislineone
SendingThisislinetwo
SendingThisislinethree
SendingThisislinefour
Disconnectingfromclient…
Exiting…    

下面是来自客户端的输出:
 
Thisislineone
Thisislinetwo
Thisislinethree
Thisislinefour     

属性和元数据

C#和C++之间一个显著的区别是它提供了对元数据的支持:有关类、对象、方法等其他实体的数据。属性可以分为二类:一类以CLR的一部分的形式出现,另一种是我们自己创建的属性,CLR属性用来支持串行化、排列和COM协同性等。一些属性是针对一个组合体的,有些属性则是针对类或界面,它们也被称作是属性目标。

将属性放在属性目标前的方括号内,属性就可以作用于它们的属性目标。
 
[assembly:AssemblyDelaySign(false)]
[assembly:AssemblyKeyFile(".\\keyFile.snk")]    

或用逗号将各个属性分开:
 
[assembly:AssemblyDelaySign(false),
assembly:AssemblyKeyFile(".\\keyFile.snk")]    

自定义的属性

我们可以任意创建自定义属性,并在认为合适的时候使用它们。假设我们需要跟踪bug的修复情况,就需要建立一个包含bug的数据库,但需要将bug报告与专门的修正情况绑定在一块儿,则可能在代码中添加如下所示的注释:
 
//Bug323fixedbyJesseLiberty1/1/2005.    

这样,在源代码中就可以一目了然地了解bug的修正情况,但如果如果把相关的资料保存在数据库中可能会更好,这样就更方便我们的查询工作了。如果所有的bug报告都使用相同的语法那就更好了,但这时我们就需要一个定制的属性了。我们可能使用下面的内容代替代码中的注释:
 
[BugFix(323,"JesseLiberty","1/1/2005")Comment="Offbyoneerror"]    

与C#中的其他元素一样,属性也是类。定制化的属性类需要继承System.Attribute:

publicclassBugFixAttribute:System.Attribute

我们需要让编译器知道这个属性可以跟什么类型的元素,我们可以通过如下的方式来指定该类型的元素:
 
[AttributeUsage(AttributeTargets.ClassMembers,AllowMultiple=true)]    

AttributeUsage是一个作用于属性的属性━━元属性,它提供的是元数据的元数据,也即有关元数据的数据。在这种情况下,我们需要传递二个参数,第一个是目标(在本例中是类成员。),第二个是表示一个给定的元素是否可以接受多于一个属性的标记。AllowMultiple的值被设置为true,意味着类成员可以有多于一个BugFixAttribute属性。如果要联合二个属性目标,可以使用OR操作符连接它们。
 
[AttributeUsage(AttributeTargets.ClassAttributeTargets.Interface,AllowMultiple=true)]    

上面的代码将使一个属性隶属于一个类或一个界面。

新的自定义属性被命名为BugFixAttribute。命名的规则是在属性名之后添加Attribute。在将属性指派给一个元素后,编译器允许我们使用精简的属性名调用这一属性。因此,下面的代码是合法的:
 
[BugFix(123,"JesseLiberty","01/01/05",Comment="Offbyone")]    

编译器将首先查找名字为BugFix的属性,如果没有发现,则查找BugFixAttribute。

每个属性必须至少有一个构造器。属性可以接受二种类型的参数:环境参数和命名参数。在前面的例子中,bugID、编程人员的名字和日期是环境参数,注释是命名参数。环境参数被传递到构造器中的,而且必须按在构造器中定义的顺序传递。
 
publicBugFixAttribute(intbugID,stringprogrammer,stringdate)
{
this.bugID=bugID;
this.programmer=programmer;
this.date=date;
}

Namedparametersareimplementedasproperties.    

属性的使用

为了对属性进行测试,我们创建一个名字为MyMath的简单类,并给它添加二个函数,然后给它指定bugfix属性。
 
[BugFixAttribute(121,"JesseLiberty","01/03/05")]

[BugFixAttribute(107,"JesseLiberty","01/04/05",
Comment="Fixedoffbyoneerrors")]
publicclassMyMath    

这些数据将与元数据存储在一起。下面是完整的源代码及其输出:

自定义属性
 
usingSystem;
//创建被指派给类成员的自定义属性
[AttributeUsage(AttributeTargets.Class,
AllowMultiple=true)]
publicclassBugFixAttribute:System.Attribute
{
//位置参数的自定义属性构造器
publicBugFixAttribute
(intbugID,
stringprogrammer,
stringdate)
{
this.bugID=bugID;
this.programmer=programmer;
this.date=date;
}
publicintBugID
{
get
{
returnbugID;
}
}

//命名参数的属性
publicstringComment
{
get
{
returncomment;
}
set
{
comment=value;
}
}

publicstringDate
{
get
{
returndate;
}
}

publicstringProgrammer
{
get
{
returnprogrammer;
}
}

//专有成员数据
privateintbugID;
privatestringcomment;
privatestringdate;
privatestringprogrammer;
}

//把属性指派给类

[BugFixAttribute(121,"JesseLiberty","01/03/05")]
[BugFixAttribute(107,"JesseLiberty","01/04/05",
Comment="Fixedoffbyoneerrors")]
publicclassMyMath
{

publicdoubleDoFunc1(doubleparam1)
{
returnparam1+DoFunc2(param1);
}

publicdoubleDoFunc2(doubleparam1)
{
returnparam1/3;
}

}

publicclassTester
{
publicstaticvoidMain()
{
MyMathmm=newMyMath();
Console.WriteLine("CallingDoFunc(7).Result:{0}",
mm.DoFunc1(7));
}
}    

输出:
 
CallingDoFunc(7).Result:9.3333333333333339    

象我们看到的那样,属性对输出绝对没有影响,创建属性也不会影响代码的性能。到目前为止,读者也只是在听我论述有关属性的问题,使用ILDASM浏览元数据,就会发现属性确实是存在的。

映射

在许多情况下,我们需要一种方法,能够从元数据中访问属性,C#提供了对映射的支持以访问元数据。通过初始化MemberInfo类型对象,System.Reflection名字空间中的这个对象可以用来发现成员的属性,对元数据进行访问。
 
System.Reflection.MemberInfoinf=typeof(MyMath);    

对MyMath类型调用typeof操作符,它返回一个由继承MemberInfo而生成的Type类型的变量。

下一步是对MemberInfo对象调用GetCustomAttributes,并将希望得到的属性的类型作为一个参数传递给GetCustomAttributes。我们将得到一个对象数组,数组的每个成员的类型都是BugFixAttribute。
 
object[]attributes;
attributes=Attribute.GetCustomAttributes(inf,typeof(BugFixAttribute));    

我们就可以遍历这个数组了,打印BugFixAttribute对象的数组,代码下所示:

属性的打印
 
publicstaticvoidMain()
{
MyMathmm=newMyMath();
Console.WriteLine("CallingDoFunc(7).Result:{0}",
mm.DoFunc1(7));

//获取成员信息并使用它访问自定义的属性
System.Reflection.MemberInfoinf=typeof(MyMath);
object[]attributes;
attributes=
Attribute.GetCustomAttributes(inf,typeof(BugFixAttribute));

//遍历所有的属性
foreach(Objectattributeinattributes)
{
BugFixAttributebfa=(BugFixAttribute)attribute;
Console.WriteLine("\nBugID:{0}",bfa.BugID);
Console.WriteLine("Programmer:{0}",bfa.Programmer);
Console.WriteLine("Date:{0}",bfa.Date);
Console.WriteLine("Comment:{0}",bfa.Comment);
}
}    

类型发现

我们可以通过映象的方法来研究一个组合实体的内容,如果要建立需要显示组合体内部信息的工具或动态地调用组合体中的途径,这一方法是非常有用的。

通过映象的方法,我们可以知道一个模块、方法、域、属性的类型,以及该类型的每个方法的信号、该类支持的界面和该类的超级类。我们可以通过如下的形式,用Assembly.Load静态方法动态地加载一个组合体:
 
publicstaticAssembly.Load(AssemblyName)    

然后,可以将它传递到核心库中。
 
Assemblya=Assembly.Load("Mscorlib.dll");    

一旦加载了组合体,我们可以通过调用GetTypes返回一个Type对象数组。Type对象是映射的核心,它表示类、界面、数组、值和枚举等的类型定义。
 
Type[]types=a.GetTypes();    

组合休会返回一个类型的数组,我们可以使用foreach-loop结构显示该数组,其输出将有好几页文档之多,下面我们从中找一小段:
 
TypeisSystem.TypeCode
TypeisSystem.Security.Util.StringExpressionSet
TypeisSystem.Text.UTF7Encoding$Encoder
TypeisSystem.ArgIterator
TypeisSystem.Runtime.Remoting.JITLookupTable
1205typesfound    

我们得到了一个内容为核心库中类型的数组,可以将它们都打印出来,该数组将有1205个项。

对一种类型映射我们也可以对组合体中一种类型进行映射。为此,我们可以使用GetType方法从组合体中解析出一个类型:
 
publicclassTester
{
publicstaticvoidMain()
{
//检查一个对象
TypetheType=Type.GetType("System.Reflection.Assembly");
Console.WriteLine("\nSingleTypeis{0}\n",theType);
}
}    

输出如下所示:
 
SingleTypeisSystem.Reflection.Assembly    

发现成员

我们还可以得到所有成员的类型,显示所有的方法、属性、域,下面的代码演示了实现上述目标的代码。
 
Figure9GettingAllMembers
publicclassTester
{
publicstaticvoidMain()
{
//检查一个单一的对象
TypetheType=Type.GetType("System.Reflection.Assembly");
Console.WriteLine("\nSingleTypeis{0}\n",theType);

//获取所有的成员
MemberInfo[]mbrInfoArray=
theType.GetMembers(BindingFlags.LookupAll);
foreach(MemberInfombrInfoinmbrInfoArray)
{
Console.WriteLine("{0}isa{1}",
mbrInfo,mbrInfo.MemberType.Format());
}
}
}    

尽管得到的输出还非常长,但在输出中我们可以得到如下面的不甘落后民示的域、方法、构造器和属性:
 
System.Strings_localFilePrefixisaField
BooleanIsDefined(System.Type)isaMethod
Void.ctor()isaConstructor
System.StringCodeBaseisaProperty
System.StringCopiedCodeBaseisaProperty    

只发现方法

我们可能会只关心方法,而不关心域、属性等,为此,我们需要删除如下的对GetMembers的调用:
 
MemberInfo[]mbrInfoArray=
theType.GetMembers(BindingFlags.LookupAll);    

然后添加调用GetMethods的语句:
 
mbrInfoArray=theType.GetMethods();    

现在,输出中就只剩下方法了。
 
Output(excerpt)
BooleanEquals(System.Object)isaMethod
System.StringToString()isaMethod
System.StringCreateQualifiedName(System.String,System.String)
isaMethod
System.Reflection.MethodInfoget_EntryPoint()isaMethod    

发现特定的成员
最后,为了进一步地缩小范围,我们可以使用FindMembers方法来发现某一类型的特定的方法。例如,在下面的代码中,我们可以只搜索以“Get”开头的方法。
 
publicclassTester
{
publicstaticvoidMain()
{
//检查一个单一的对象
TypetheType=Type.GetType("System.Reflection.Assembly");
//只获取以Get开头的成员
MemberInfo[]mbrInfoArray
theType.FindMembers(MemberTypes.Method,
BindingFlags.Default,
Type.FilterName,"Get*");
foreach(MemberInfombrInfoinmbrInfoArray)
{
Console.WriteLine("{0}isa{1}",
mbrInfo,mbrInfo.MemberType.Format());
}
}
}    

其输出的一部分如下所示:
 
System.Type[]GetTypes()isaMethod
System.Type[]GetExportedTypes()isaMethod
System.TypeGetType(System.String,Boolean)isaMethod
System.TypeGetType(System.String)isaMethod
System.Reflection.AssemblyNameGetName(Boolean)isaMethod
System.Reflection.AssemblyNameGetName()isaMethod
Int32GetHashCode()isaMethod
System.Reflection.AssemblyGetAssembly(System.Type)isaMethod
System.TypeGetType(System.String,Boolean,Boolean)isaMethod    

动态调用

一旦发现一个方法,可以使用映射的方法调用它。例如,我们可能需要调用System.Math中的Cos方法(返回一个角的余弦值)。为此,我们需要获得System.Math类的类型信息,如下所示:
 
TypetheMathType=Type.GetType("System.Math");    

有了类型信息,我们就可以动态地加载一个类的实例:
 
ObjecttheObj=Activator.CreateInstance(theMathType);    

CreateInstance是Activator类的一个静态方法,可以用来对对象进行初始化。

有了System.Math类的实例后,我们就可以调用Cos方法了。我们还需要准备好一个定义参数类型的数组,因为Cos只需要一个参数(需要求余弦值的角度),因此数组中只需要有一个成员。我们将在数组中赋予一个System.Double类型的Type对象,也就是Cos方法需要的参数的类型:
 
Type[]paramTypes=newType[1];
paramTypes[0]=Type.GetType("System.Double");    

现在我们就可以传递方法的名字了,这个数组定义了Type对象中GetMethod方法的参数的类型:
 
MethodInfoCosineInfo=
theMathType.GetMethod("Cos",paramTypes);    

我们现在得到了MethodInfo类型的对象,我们可以在其上调用相应的方法。为此,我们需要再次在数组中传入参数的实际值:
 
Object[]parameters=newObject[1];
parameters[0]=45;
ObjectreturnVal=CosineInfo.Invoke(theObj,parameters);    

需要注意的是,我创建了二个数组,第一个名字为paramTypes的数组存储着参数的类型,第二个名字为parameters的数组保存实际的参数值。如果方法需要二个参数,我们就需要使这二个数组每个保持二个参数。如果方法不需要参数,我们仍然需要创建这二个数组,只是无需在里面存储数据即可。
 
Type[]paramTypes=newType[0];    

尽管看起来有点奇怪,但它是正确的。下面是完整的代码:

映射方法的使用
 
usingSystem;
usingSystem.Reflection;publicclassTester
{
publicstaticvoidMain()
{
TypetheMathType=Type.GetType("System.Math");
ObjecttheObj=Activator.CreateInstance(theMathType);

//只有一个成员的数组
Type[]paramTypes=newType[1];
paramTypes[0]=Type.GetType("System.Double");

//获得Cos()方法的信息
MethodInfoCosineInfo=
theMathType.GetMethod("Cos",paramTypes);

//将实际的参数填写在一个数组中
Object[]parameters=newObject[1];
parameters[0]=45;
ObjectreturnVal=CosineInfo.Invoke(theObj,parameters);
Console.WriteLine(
"Thecosineofa45degreeangle{0}",returnVal);

}
}    

结论

尽管有许多小错误等着C++编程人员去犯,但C#的语法与C++并没有太大的不同,向新语言的转换是相当容易的。使用C#的有趣的部分是使用通用语言运行库,这篇文章只能涉及几个重点问题。CLR和.NETFramework提供了对线程、集合、互联网应用开发、基于Windows的应用开发等方面提供了更多的支持。语言功能和CLR功能之间的区分是非常模糊的,但组合在一起就是一种功能非常强大的开发工具了。