{ Snake @ NET } 废寝忘食的程序工作者.

19八/100

用接口与委托的常用方法来谈二者的区别

早先的一篇文章让我与几个朋友引发了一些讨论,大部分都在谈论的表达能力的问题,于是我在这一篇重整语言,并且将二者的部分通用方法列举出来再次试图表明他们的区别.希望我这次的行文会更容易让人接受一些.废话不多说,正题开始.

1. 困惑

接口与委托之所以出现混淆,是因为二者都能影响某一个方法的执行结果或执行过程,写一个极为简单的例子就能看出他们的相似:

public class DeleTest
{
	public DeleTest(decimal a, decimal b)
	{
		this.a=a;
		this.b=b;
	}
	private decimal a,b;
	//这是一个通过传递一个Func<T1,T2,TResult>委托来计算值的方法.
	public decimal Calculate(Func<decimal, decimal, decimal> func)
	{
		return func(a,b);
	}
	//下面是2个匹配Func<T1,T2,TResult>的方法.
	public decimal Add(decimal a,decimal b)
	{
		return a+b;
	}
	public decimal Substract(decimal a,decimal b)
	{
		return a-b;
	}
	//我们通过以下方式调用来改变Calculate的值
	public static void Main(string[] args)
	{
		DeleTest t=new DeleTest(1,2);
		Console.WriteLine(t.Calculate(t.Add));		//结果为3
		Console.WriteLine(t.Calculate(t.Substract));//结果为-1
	}
}

DeleTest类是一个委托的示范类,其中包含2个符合Func<T1,T2,TResult>签名的方法Add,Substract.当我们调用该类的Calculate方法时,需要传进一个委托,而整个Calculate方法完全依靠这个委托来获得计算结果.

public interface ICal
{
	decimal Calculate(decimal a,decimal b);
}
public class Addition
{
	public decimal Calculate(decimal a, decimal b)
	{
		return a+b;
	}
}
public class Substraction
{
	public decimal Calculate(decimal a, decimal b)
	{
		return a-b;
	}
}
public class IfTest
{
	public static void Main(string[] args)
	{
		decimal a=1,b=2;
		ICal ical=new Addition();
		Console.WriteLine(ical.Calculate(a,b)); //相加得3
		ical=new Substraction();
		Console.WriteLine(ical.Calculate(a,b)); //相减得-1
	}
}

以上是接口的实现版本,首先定义一个ICal接口,实现ICal接口的类都必须实现Caculate方法.此时接口给我们提供了一个不变切入点.只要我们声明一个ICal接口成员,并且赋一个合法的值(即实现了ICal接口的类),那么这个类一定包含了我们想要调用Calculate方法.于是在改变其ical变量的引用时,输出的结果当然不同.好比通过以下代码:

string a="snake";

a="cat;"

a最终的值是什么一样简单.

那么我们仔细研究一下2者的区别在何处.

委托是.NET平台中一个独特的东西,它可以向指针一样把方法当做变量(或参数).但是委托不但能调动一个方法,还能顺序调用多个方法,而实现方式只要:

委托+=方法,匿名方法,Lambda表达式;

就可以轻松实现.

另外委托还有异步调用的功能,使用:

委托.BeginInvoke()

方法即可通过异步方式执行委托,实现多线程操作.这些都是委托极为醒目的特点.

相比接口,接口并不是一个新事物,但它却是传统面对对象语言所不可或缺的一个元素.

类需要强制要求实现它的类必须实现它所声明的一切(包括属性,方法等),此时在应用程序中,只要声明了一个接口成员,那么接口背后的实例就绝对实现了接口中的一切.此时应用程序就可以毫无顾虑地调用这个接口成员的指定方法(如上例的ical.Calculate方法).

2. 解疑

俗话说条条大路通罗马,但是我们在编程中需要极力避免出现南辕北辙耗时耗力的错误.虽然在Java中事件是基于接口形式实现的,但在.NET平台下的语言中,事件则是基于委托.使用委托能大量节省事件的实现步骤.另外,因为委托和接口在.NET平台下的关注点不同,所以二者所胜任的工作也是大不相同的.下面我就通过二者的适用情况来解释二者的区别.

l 委托关注点在方法,作用范围小.

l 接口关注点在于类的设计,作用范围广.

2.1 委托

关注点:

不止读者您有没注意到在设计一个方法的时候我们可以随意安插委托的位置,使该委托能够发挥它的效力.我们不妨小小模拟一下集合IList<T>中的扩展方法OrderBy的实现策略(OrderBy方法能通过类型T的某一成员进行重新排序):

public void OrderBy<T>(this IList<T> list,Func<T1, TResult> dele)
{
	T temp;
	for(int i=0;i<list.Count;i++)
	{
		for(int j=0;j<list.Count;j++)
		{
			if(dele(list[i])>dele(list[j]))
			{
				temp=list[j];
				list[j]=list[i];
				list[i]=temp;
			}
		}
	}
}

上面的if(dele(list[i])>dele(list[j]))就是通过委托来获取类型T的某一成员的值来互相比较这时我就可以这样调用OrderBy方法:

//设一类型为projectList集合proj,需要通过project.ConstructionTime(施工期)的长//短来排序

proj.OrderBy(p => p.ConstructionTime);//Lambda表达式的调用方式

//通过匿名方法来调用该方法则为:

proj.OrderBy(new delegate(p){

return p.ConstructionTime;

});

我们可以看到委托是生存于方法内的.明确的说,委托在活动期间是与方法相关联的.我们在适当地地方调用委托,让委托发挥效应从而改变方法的执行结果.

作用范围:

在最初的计算例子中我们能看到,当我们以方法为参数的时候其实只传递了一个方法签名(t.Calculate(t.Add).)我们并没有向往常一样看到方法的常规结尾闭合括弧( [参数们] )”.因为委托只负责调用方法,至于如何传递参数,那是调用委托的负责人所要干的事情.

不妨把调用委托的方法称为老板,把委托称为秘书,把被委托调用的方法称为员工.

这时老板想到了一个点子需要能赚到一笔不小的生意,但是自己都已经身为老板不能什么都自己揽着干,找了那么多小弟也不是白给工资的.于是老板通过秘书,让秘书将老板的要求传达给他手下的员工们做,最后他只要能赚到这笔钱就成了,管他是老板干的还是员工干的.

2.2接口

关注点:

接口的关注点在于类的设计.它约束类必须实现接口所声明的一切.这样才能有一个不变的调用途径.就像所有实现了IEnumerable接口的类都可以通过foreach语句进行迭代,还有所有实现了IDisposable接口的类都有一个Dispose方法可供使用者进行资源释放,而使用using语句更是能简化这一操作.这是一个类所具有的特征,而委托就无法在做到这种效果.

另外,.NET3.0以后在声明泛型类或方法的时候,我们可以在类名或方法签名之后使用 where语句来限制T的类型,避免错误传递类型引发应用程序错误.

作用范围:

接口声明的成员都在类中实现,所以无论是属性还是方法,他们都有很大的自由度.比如截然不同的数据库存储模式,虽然通过接口我们都能获取到一致的结果,但是二者的实现方式和可利用的资源绝对不仅仅只有委托签名中仅有的几个参数而已.

//定义一个实体类
public class Entity
{
	Guid ID{get;set;}
}

//这是我们数据库连接层接口
public interface IDataAccessLayer
{
    void Add(Entity e);
    void Update(Entity e);
    void Delete(entity e);
    Entity SelectAll();
}

//MySql数据库连接层
public class MySqlDAL : IDataAccessLayer
{
    private static MySqlDataContext c = new MySqlDataContext("MySql的连接字符串");
    public void Add(Entity e)
    {
        string addString = "insert ***";
        //这里包含了各种sql语句生成内容,不一一编造.
        c.ExecuteCommand(addString);
    }
    //其他方法就不写了,咱们一个方法就能说明问题.
}

//Oracle数据库连接层
public class OracleDAL : IDataAccessLayer
{
    private static OracleDataBase db = new OracleDataBase("Oracle连接字符串");
    private static Table<Entity> table = db.GetTable("Entity");

    public void Add(Entity e)
    {
        table.Add(e);
        db.SaveChanges();
    }
}

//客户端上的实现方案
public class Client
{
    IDataAccessLayer dal;

//构造器,通过传入不同的字符串来支持不同的数据库类型.
    public Client(string dbType)
    {
       switch (dbType)
        {
           case "mysql":
               dal = new MySqlDAL();
           case "oracle":
               dal = new OracleDAL();
           default:
               throw new System.NotSupportedException("We're not supported the " + dbType + " Database");
        }
    }

//通过这个属性来进行数据库操作.
    public IDataAccessLayer DAL{ get { return dal; } }

    public static void Main()
    {
        Client c=new Client("oracle");
        c.DAL.Add(new Entity()
        {
            ID = "new item"
        });
    }
}

//以上为伪代码,不可执行

可以通过上面稍微复杂的代码看到,2种不同的类虽然实现了同一接口,但是他们完全调用了所有类中能调用的一切资源进行操作.而这么庞大的操作,并不是委托的强项.

3.总结

我们首先通过委托与接口二者的相似之处开始展开,又有因为细节部分而将二者区分开来,也提到了而且完全不同的地方.

本文提到通过编程语言编写一个功能不是不可能,只是实现方法的问题,我们需要利用编程语言和平台的每个对象的优点和特性来决定最适合他们应用方向,切忌南辕北辙.所以最后本文通过两个最为重要的重点解释和区分了委托和接口二者的区别及适用场景.

最后祝大家事业顺利!

8八/100

关于List和LinkedList的小测试

最近接触到了一个关于大集合定期轮询扫描的功能,因为大集合的遍历和判断可能会消耗比较多的时间和CPU资源,于是我准备根据.Net平台GC设计的代龄理念搞一个GenerationScaner.当然,不论它是否真的有效,这只是个计划.计划大概是这样的.

类中根据每个item的紧急情况(如某Item快要过期,我要发一条短信通知用户)分成3个集合,这3个集合会以不同的频率定期扫描集合内所有的item并且重新排列.第三代龄里总有些item因为时间的流逝(或其他原因)而进入第二代龄,再进入第一代龄.就像盛满水的三个不同高度的碗,最上面那个装满了就会将水流到下面一个,再到最后一个碗.

而在思考关于3个代龄的集合的实现类型的时候,我在List<T>和LinkedList<T>上思考了一会,主要是因为我觉得在3个集合中items并不是因各种特定的条件排序过的,所以我们在筛选第三代龄的item并且把筛选后的item放到第二代龄中去的时候就会面临着集合的乱序删除.

在数组的这种状态下,如果有10个项目,删除第1个,则后面9个项目都要向前移动1位,可能造成性能损失.而List<T>就是基于数组而实现的.对于链表来说,乱序删除就显得不是那么恐怖,如果有10个项目在一个链表内,删除第3个项目,则链表会讲第2个项目的Next引用指向第4个项目,而不用再移动任何项目.

我也在博客园中找了不少关于这两个集合的测试,但是关于链表的乱序删除这个强项却没有有力的测试.于是我决定自己试一试.

首先我写一个类Test包含一个List<int>和LinkedList<int>;

其次我在AppendTest方法中以一个向两者分别插入一千万条数据,这些数据是Random函数随机生成的数字.

然后我想测试一下二者纯遍历所需要的时间.

最后测试二者的乱序删除.

代码如下.

class Program
{
    public class Test
    {
        public Test(int count, int condition)
        {
            this.count = count;
            this.condition = condition;
            this.list = new List<int>();
            this.ll = new LinkedList<int>();
        }

        int condition = 0;
        int count = 0;
        List<int> list;
        LinkedList<int> ll;
        Random r = new Random(DateTime.Now.Millisecond);
        Stopwatch sw = new Stopwatch();

        private int GetRandom()
        {
            return r.Next(0, count);
        }

        public void AppendTest()
        {
            Print("+--- " + count + " AppendTest: ---+");

            long listTakes = 0;
            long llTakes = 0;
            sw.Reset();
            sw.Start();
            for (int i = 0 ; i < count ; i++)
            {
                list.Add(GetRandom());
            }
            sw.Stop();
            listTakes = sw.ElapsedMilliseconds;

            sw.Reset();
            sw.Start();
            for (int i = 0 ; i < count ; i++)
            {
                ll.AddLast(GetRandom());
            }
            sw.Stop();
            llTakes = sw.ElapsedMilliseconds;

            Print("\tList takes  " + listTakes + "  milliseconds to append.");
            Print("\tLinkedList takes  " + llTakes + "  milliseconds to append.");
            Print("+--- Test Finished ---+");
        }

        public void ForEachTest()
        {
            Print("+--- " + count + " ForEachTest: ---+");

            long listTakes = 0;
            long llTakes = 0;
            sw.Reset();
            sw.Start();
            foreach (var item in list)
            {
            }
            sw.Stop();
            listTakes = sw.ElapsedMilliseconds;
            sw.Reset();
            sw.Start();
            foreach (var item in ll)
            {
            }
            sw.Stop();
            llTakes = sw.ElapsedMilliseconds;

            Print("\tList takes  " + listTakes + "  milliseconds to iterating.");
            Print("\tLinkedList takes  " + llTakes + "  milliseconds to iterating.");
            Print("+--- Test Finished ---+");
        }

        public void RemoveTest()
        {
            Print("+--- " + count + " RemoveTest: ---+");
            Print("Both of the collection will remove the item less than " + condition + ".");

            long listTakes = 0;
            long llTakes = 0;
            int listRemoved = 0;
            int llRemoved = 0;
            sw.Reset();
            listRemoved = list.Count;
            sw.Start();
            list.RemoveAll(delegate(int i) { return i < condition; });
            sw.Stop();
            listRemoved -= list.Count;
            listTakes = sw.ElapsedMilliseconds;
            sw.Reset();
            sw.Start();
            var next = ll.First;
            while (next != null)
            {
                if (next.Value < condition)
                {
                    var item = next;
                    next = next.Next;
                    ll.Remove(item);
                    llRemoved++;
                }
                else
                    next = next.Next;
            }
            sw.Stop();
            llTakes = sw.ElapsedMilliseconds;

            Print("\tList takes  " + listTakes + "  milliseconds to removing,and removed " + listRemoved + " items.");
            Print("\tLinkedList takes  " + llTakes + "  milliseconds to removing,and removed " + llRemoved + " items.");
            Print("+--- Test Finished ---+");
        }
    }

    public static void Print(string s)
    {
        Console.WriteLine(s);
    }
    public static void Main(string[] args)
    {
        Test t = new Test(10000000, 10000);

        t.AppendTest();
        Console.Write("\nPress any key to continue . . . \n");
        Console.ReadKey(true);

        t.ForEachTest();
        Console.Write("\nPress any key to continue . . . \n");
        Console.ReadKey(true);

        t.RemoveTest();
        Console.Write("\nPress any key to continue . . . \n");
        Console.ReadKey(true);
    }
}

这些代码应该很好看懂我就不写注释了,主要是最后的RemoveTest中我所使用的两种方法来删除项目,因为foreach循环中不可改变集合的内容.

开始测试.

首先保证代码是Release形式的.

其次测试的机子配置如下:

  • CPU: AMD 5000+ BE
  • RAM: Kingston 800M 2G *2
  • MotherBoard: Colorful AMD 780G

最后上运行结果:

第一次测试:

+--- 10,000,000 AppendTest: ---+
List takes 551 milliseconds to append.
LinkedList takes 4,852 milliseconds to append.
+--- Test Finished ---+

+--- 10,000,000 ForEachTest: ---+
List takes 40 milliseconds to iterating.
LinkedList takes 102 milliseconds to iterating.
+--- Test Finished ---+

+--- 10,000,000 RemoveTest: ---+
Both of the collection will remove the item less than 100,000.
List takes 89 milliseconds to removing,and removed 99,808 items.
LinkedList takes 102 milliseconds to removing,and removed 10,0519 items.
+--- Test Finished ---+

第二次测试:

+--- 10,000,000 AppendTest: ---+
List takes 560 milliseconds to append.
LinkedList takes 5,030 milliseconds to append.
+--- Test Finished ---+

+--- 10,000,000 ForEachTest: ---+
List takes 42 milliseconds to iterating.
LinkedList takes 119 milliseconds to iterating.
+--- Test Finished ---+

+--- 10,000,000 RemoveTest: ---+
Both of the collection will remove the item less than 100,000.
List takes 99 milliseconds to removing,and removed 99,779 items.
LinkedList takes 108 milliseconds to removing,and removed 99,567 items.
+--- Test Finished ---+

第三次测试:

+--- 10,000,000 AppendTest: ---+
List takes 561 milliseconds to append.
LinkedList takes 4,971 milliseconds to append.
+--- Test Finished ---+

+--- 10,000,000 ForEachTest: ---+
List takes 39 milliseconds to iterating.
LinkedList takes 104 milliseconds to iterating.
+--- Test Finished ---+

+--- 10,000,000 RemoveTest: ---+
Both of the collection will remove the item less than 100000.
List takes 90 milliseconds to removing,and removed 99,862 items.
LinkedList takes 101 milliseconds to removing,and removed 100,418 items.
+--- Test Finished ---+

结果:

对于LinkedList<T>的乱序删除我感觉很意外,它的最强项之一竟然也会输给List<T>(不排除我写的LinkedList<T>乱序删除的算法太烂的原因),而添加项目的耗时竟然比List<T>多出8倍多.而且单纯遍历,List<T>表现的也比LinkedList<T>好很多.

所以基于目前简单的测试来看,我的代龄集合应该全部由List<T>这个具有优良的性能又提供了一大堆方便的处理函数的集合来打造!

最后,对于LinkedList<T>的乱序添加我没有测试,因为自己用不到.所以如果需要这方面测试的朋友不妨自己试一下 : )

2八/100

写给会混淆委托(代理)和接口概念和用途的朋友们

昨天在和一位朋友讨论到委托与接口的问题,一开始我觉得很不可思议,这两个东西的概念怎么会混淆呢?要混淆也是接口和抽象类,委托和事件相混淆啊!但是着我的一个例子我马上意识到很有可能因为我将要表现的这个例子,让很多朋友混淆了委托与接口的用途.所以我想通过这篇文章试图说明白委托和接口的概念和用途,其实他们俩的差别还是很大的.

  • 本文适合对委托和接口概念或用途不了解的朋友.
  • 本文适合对委托和接口概念非常了解的朋友,并且欢迎各位朋友与Snake一起探讨有关这方面的知识.
  • 本文不适合对委托和接口概念或用途了解一知半解(模糊)的朋友,这篇文章可能会对您产生误导,请千万别看.

(本文原文是一篇没有好好排版过的email,我这里将会部分摘抄,部分改进,如果有什么地方您觉得莫名其妙,我将非常感谢您的指正!)

在文章正式开始之前我需要将MSDN上对委托和接口的内容放上来,作为文章之基.

委托:

委托是一种定义方法签名的类型。当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联。您可以通过委托实例调用方法。

委托用于将方法作为参数传递给其他方法。事件处理程序就是通过委托调用的方法。您可以创建一个自定义方法,当发生特定事件时某个类(例如 Windows 控件)就可以调用您的方法.

委托具有以下特点:

  • 委托类似于 C++ 函数指针,但它们是类型安全的。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起;例如,可以对一个事件调用多个方法。
  • 方法不必与委托签名完全匹配。有关更多信息,请参见在委托中使用变体(C# 和 Visual Basic)
  • C# 2.0 版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法。C# 3.0 引入了 Lambda 表达式,利用它们可以更简练地编写内联代码块。匿名方法和 Lambda 表达式(在某些上下文中)都可编译为委托类型。这些功能统称为匿名函数。有关 Lambda 表达式的更多信息,请参见Anonymous Functions (C# Programming Guide)

接口:

接口描述的是可属于任何结构的一组相关功能。接口可由方法、属性、事件、索引器或这四种成员类型的任意组合构成。接口不能包含字段。接口成员一定是公共的。

当类或结构继承接口时,意味着该类或结构为该接口定义的所有成员提供实现。接口本身不提供类或结构能够以继承基类功能的方式继承的任何功能。但是,如果基类实现接口,派生类将继承该实现。

类和结构可以按照类继承基类或结构的类似方式继承接口,但有两个例外:

  • 类或结构可继承多个接口。
  • 类或结构继承接口时,仅继承方法名称和签名,因为接口本身不包含实现。

接口具有下列属性:

  • 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
  • 不能直接实例化接口。
  • 接口可以包含事件、索引器、方法和属性。
  • 接口不包含方法的实现。
  • 类和结构可从多个接口继承。
  • 接口自身可从多个接口继承。

正文开始

在写这些文字的时候我又将以上的各个概念熟悉了一遍,以防自己把自己忽悠混淆了.所以不适合群众请尽快退散.另外如果您看完上面的定义和特征后就从两者的混淆中走了出来,您也可以尝试继续往下看.

首先,关于委托的用法,我们可以这样使用:

public int Calculate(Func<int, int, int> del)
{
    int a = 1, b = 2;
    return del(a, b);
}

我们可以通过传不同的Func来改变整个方法的结果.

public int Add(int a, int b)
{ return a + b; }
public int Sub(int a, int b)
{ return a - b; }
//调用方法如下
public void TestMethod()
{
    int result = Calculate(Add);//the result is 3
    int anotherResult = Calculate(Sub);//the result is -1
}

首先我在Calculate方法中已经确定了2个数的值,并且包括在该方法当中.在输出结果的时候能明显看出传递的委托不同,其结果也不同.我们使用委托来改变方法的执行内容,我们不但可以改变其方法的内容,也可以在执行该方法的时候顺便做点什么(比如说做个日志记录).

噢,可能您觉得二者容易混淆的地方在于..我还是举个例子比较好解释. : )

public interface ICal
{
    int Calculate(int a, int b);
}
//有多个类实现了ICal接口.
public class Add : ICal
{
    public int Calculate(int a, int b)
    { return a + b; }
}
public class Sub : ICal
{
    public int Calculate(int a, int b)
    { return a - b; }
}
//然后通过调用不同类来获取不同的方法
public static void Main()
{
    ICal cal = new Add();
    //ICal=new Sub();
    Console.Write(cal.Calculate(1, 2));
}

讲解一下,通过上面的例子我们可以知道在创建一个具有计算功能(Calculate)的接口ICal之后,产生了两个具有计算功能的具体类,分别是Add和Sub.为了要获得结果,我们创建了一个需要有计算功能的"坑",并赋予能与此"坑"相匹配的类Add(或Sub),最后从该坑中调用Calculate的结果就行.

貌似说的过去?好,那么我至少要让你觉得有个适用范围吧!看下面的例子.

比如有个Person类的数组arr.这时候我们可以通过委托的方法实现arr的排序.可是系统怎么知道2个Person哪个排在前面,哪个该排在后面?这时候我们就可以传进一个委托来告诉系统Peron类的大小.

arr.Sort(p =>
{
    p.ID
});

好吧,它本来是很麻烦的:

private int SortDelegate(Person p)
{
    return p.ID;
}
public void TestMethod()
{
    arr.Sort(new Func<Person, int>(SortDelegate));
}

但是我们要承认C# 3.0带给我们的便利.

该lambda表达式意思是丢给该Sort方法一个排序的Key(此key能够进行大小比较),那么Sort就可以根据此key来进行比较.那通过接口呢?首先得创建一个继承自IComparer<Person>的类,我就拿本身继承它吧.

我们要让Person类实现接口的规定.

public int Compare(Person x, Person y)
{
    //假设person的ID是int类型
    return x.ID - y.ID;
}

那么我们的实现方法就可能是这样:

arr.Sort((new Person() as IComparer<Peron>) comp);

不能再继续举例子了,我承认我忽悠您了.这些看上去都可以的实现方法有本质的区别!

首先我们看第一个委托例子:在Calculate时我们的委托被允许使用了该方法内的两个变量a,b从而改变了整个方法的结果.在整个过程中委托时很被动的,因为它不知道自己会在什么时候被触发.上面的例子很简单,使您没有这种感觉,而且前面说过在方法执行的时候当委托被触发我们可以干点别的,比如说做个日志记录什么的,此时接口有能力又不破坏方法本身运行结构,又能做日志记录吗?显然实现了接口的类只能重写一遍该方法.

路人甲:那我在接口的实现中再调用一下原方法,最后在方法的前面或后面加入日志记录功能不就完了吗?

Snake:杀鸡焉用宰牛刀?且不说再原方法的可行性,就算可行了,麻烦不说,万一这个方法执行有多个阶段,每个阶段都要日志记录呢?委托能深入方法,并且由方法控制它安放之地,让委托能起到关键作用,此时作为接口大哥的牛刀也剔不干净鸡骨上的肉哟~.

其次说接口的优点.我们前面可以看到委托能深入方法,也就是说委托的关注群体是方法们,而接口关注的群体则是类们.接口让类必须实现相同签名的方法或属性,以便在程序中通过调用可变的方法.既然是因为类的关系,那么它的方法肯定是不可变的了,每个实现了该接口的类,即便功能差不多也要完完全全写一遍,但是类的地盘大,肚子里的墨水多,虽然在Add类中通过ICal可调用的方法也就一个Calculate(),但是在Calculate始终是Add类的子民,所以该Calculate方法可以调用Add类中所有能调用的资源.而如果是Sub类的话,它的子民Caculate可调用的资源又与Add类不尽相同,毕竟同是Calculate,国籍不同,文化和生活方式也不同嘛,哈哈.

而接口的能力却是委托所不能企及的地方.它只能被方法藏在伸出,方法外一片蓝天而它却无能为力.如果让类比作一个国家,方法比作一个人,那么委托不就是深藏在人大脑内的处理方式的思维吗?不同的人,思维可以变,当乡下人看到城市中的高楼大厦不禁感叹,可乡下人在城市中生活习惯之后,高楼大厦又能怎样,他早已习以为常.

最后的论点有点晦涩,前面的例子具有误导性,所以本篇文章需要读懂个人认为不是很容易,毕竟个人对于表达能力还是比较不自信的.希望各位同仁海涵.

16五/107

[记录]乱数假文生成器的制作过程

我的乱数假文生成器已经正式发布了访问地址为:http://bugunow.com/lipsum

起因...

前两天在为系的网站设计样式稿,当我觉得页面太空了想要塞点文字的时候,花了不少时间找素材.这时候我想到当初我在一位台胞的博客上看到一篇有关假文的生成工具(具体我已经不记得网址了),于是我翻山越岭的找啊,找到了没有中文的LoremIpsum和只有繁体中文的乱数假文生成器.或许我不知道它在大陆还有什么别的叫法,于是激情一燃就不可收拾,完全忘了Google Translate就是最好的假文生成利器...

什么是乱数假文生成器?

如果非要给它下一个定义的话,乱数假文生成器就是一个:能生成一定长度的没有意义,但乍一眼望去又像极了自然文字的工具.

具体内容点这里查看.

开始动手.

在没有分析任何中英文差别下,我开始了编码工作,也因为时间的关系我并没有在如何实现这些功能的性能上考虑太多.

因为上面的原因给我这个网站埋下了2个伏笔,特别是性能上.这些稍候再说.

考虑1:在生成之前,这些"汉字"从哪里来?

个人觉得这些汉字应该要存在某个"汉字库"里面,从汉字库里随机抽取汉字堆叠生成一段假文.于是我决定做一个"字库",存放一个数量级的没有重复的汉字.那么在做生成器之前还要做一个字库存储器.突然觉得别看是一个小东西,但是真正实现起来还着实麻烦呢!

注意:我在这里其实犯了一个错,并且现在才恍然大悟.那么我将会在揭示伏笔的时候一起道出这个让我南辕北辙并且现在还没有实现的功能吧.

编写字库这个程序的时候是我最花时间的时候,因为自己都在做web开发,很少做winform开发,所以一开始搞winform的时候就拉了好多有的没的的控件,为了它的美观花了不少功夫.其次,在编写代码的时候我非常注意OO,并且尽量让每个Method中的代码不超过10行,做了好多防止误操作的检测,还大玩特玩起了事件机制.所以结果由此而知,我竟然做出了一个"产品"出来,而不是只做一个自己临时用一用的小工具而已.

动手1:实现字库的思路如下:

1.首先写了一个匹配英文字母和空格的正则表达式,写了一个平常经常使用的标点符号数组.

2.因为我只想让字库拥有"汉字"这唯一的属性,所以就定义了一个List<char>作为字库来所有的"汉字"

3.从外部txt文件中读取文档作为Source.

4.去除Source中所有的"非法"字符,空白,和英文单词.

5.循环Source中的每一个"汉字"并且查看字库中是否已经含有这个汉字,如果字库中没有这个汉字的话,则添入,否则继续循环.

6.循环结束并且序列化成二进制文档保存以备后需.

就这6跳步骤,我1个小时完全就能做出来了,出了思考的时间之外,就是上面说过的,太追求"完美"了..

动手2:导入字库.

上网下载了一个长达6MB多的txt小说扔进"工厂"里加工之后字库的汉字数量由0变成了3500多不由得意了一下.不过,耗时1分多钟....

此时进入生成器编码阶段,我流利地新建了一个工程(类库),决定要把它做到足够通用.

思考2:生成器的流程:

1.构造一个Lorem Ipsum类,给出汉字字库和英文词库的path,再给出换行符符号(因为在web换行是<br/>而在win下,换行符是\n).

2.执行制造假文Method,并传进一个LoremIpsumModel参数(实体类)里面包含了各种生成选项.

3.根据参数计算出假文应该有多少段落,并且每段多少个字.

4.开始随机取汉字堆假文.并且在段落的最后加入"结束标点符号"和换行符.

5.根据参数插入零星英文单词和标点符号.

6.返回假文.

由上面的流程可以看出将要做出以下几个东西:

1.中英文标点常量,并且还要区分结束和非结束标点.

2.英文字库(真悲剧现在才想到).

3.假文分段的算法,计算英文单词个数的算法,标点符号个数的算法(包括在方圆几个汉字之内不能重复出现标点).

悲剧,因为考虑到英文单词更能表现一个字体的全能性(中英文都好看),于是停下了手中写生成器的工作,开始写英文词库.

英文词库处理工具比汉字字库要来的方便的多,一个正则就能揪出所有的英文单词,为了效率,这个工具写的很脆弱,花了半小时,之后立马删除了.

最后制作生成器除了考虑通用性之外,很迅速的就写出了具体的生成方法,在winform和web上都能通用让我快乐,终于也跨"平台"了一次!呵呵..

动手3:测试假文生成器.

几次debug搞定死循环错误之后,第一次测试非常的不理想.虽然的确如我所愿生成了一篇假文,500个字,3个自然段,标点正常.结果一大堆乱七八糟的字,通篇下来看不到一个"你"或者"我"字.并且那些字的笔画也太多了吧,即便是天书也不能这么天书,太不真实了.

此时才开始思考中英文差别的问题,英文是拼音文字,n个字母才组成一个单词,并且由空格分开,虽然占的空间多,但是比较优美,通篇胡乱的英文乍一看上去还是非常真实的(也可能因为本人英文阅读水平太差,门外汉看不出行内的好坏),因为都是以单词的形式呈现出来的.

简体中文在使用时笔画繁多的汉字不多,并且有时候一个汉字包含了许多意思,并且不常用到.那本小说大概神神鬼鬼的内容写了太多,字虽然有3500多个,但是总体来说效果不好.我想,我们最最常用的汉字有多少个呢?1000多左右吧,多余的2500多个应该算是使用频率不是那么频繁或者根本就不频繁的汉字.于是我决定删掉汉字字库,再导一个.

这次目标盯上了鲁迅和朱自清两位老先生的作品,但是实践结果是:不够白话文一些,还是不理想!最后决定在博客上下手,自己的博文,老徐的,老赵的,老七老八的文章复制了大概26k左右,导入了1200多个字.这才有了不错的效果!

伏笔之一:没有分析中英文差距和中文特色导致花费很多时间在调整字库上,到此终于解决.

伏笔二:性能.做了一次非常非常简陋的测试,循环10000次生成1000字的假文花了7秒多..还是比较久的,并且CPU占用率超过70%.还好这个工具不是特别多人用,而且我的工具目前访问人数不是特别多,也就先不担心了.

最后花了点时间微调标点符号和段落的算法,开始搞网站了.网站因为功能单一没啥好说的,只是花了不少时间在设计上面.现在也在上传到主机的途中.

说说缺点:

1.没有根据汉字的使用率频繁与否生成一段更真实的假文.

之前还在想要不要做一个Key Value  Pair,key里放的字汉字,Value放的是频率,对比重复一次自加1,然后根据一定算法获取常用字.这是时间换空间的做法.但个人认为根本没这必要(写这篇文章的时候才想到的).首先字库再怎么大也超不过10M,10M的txt文本至少也有上千万个字吧,这种业务,字库放个50000字就算小题大做了,那么生成的字库文件也不过50多k吧.另外,获取数组的index位的时间复杂度是O(1),不会因为字库的文字多而导致性能问题.一个字库重复的字多了,被选中的几率就搞了,代码写起来也简单了,内存和硬盘空间的占用也完全不是问题.那么我何必还大费周章去搞汉字字库处理工具呢?Orz....

update at 2010/5/16 10:00:使用以上的思路重新导入了一趟字库,感觉效果还是不错滴.

2.没有可读性.

我也很想让这些文字看来有一定的可阅读性,也就是具有至少"主,谓,宾"三个关键组成部分.但是这样就要给所有字库里面的文字添加这些属性.另外小弟不才暂时没有想到很好的批处理办法.所以这个功能等有思路能实现一个比较省时的方法的时候再来做吧.

3.功能不多.

是,国外的还能生成N个段落,N个字,列表的N个项目等等..这些功能非常简单,未来将会再添加上.

在最后发布到服务器的时候出现了System.Security.SecurityPermisson的问题,从堆栈错误上来看,是反序列化时产生的问题,折腾了半天不知其所以然来,所以决定明天再修改一下程序,降低访问权限,毕竟godaddy上客户是待宰的羔羊..

13五/101

Winform跨线程操作控件的办法.

今天写了个程序,碰到了这个问题:

线程间操作无效: 从不是创建控件 [控件名称]  的线程访问它。

我现在正在使用vs2010英文版,平常也很少看那些有的没的文字,而且在写程序的时候很"明智"的异常捕获并且写进日志里去,所以没有出现那个可爱的调试状态下出现异常的那个小框框.

我在上网查了一会儿,有人说直接设置控件的CheckForIllegalCrossThreadCalls属性为false,但是因为可能的不安全因素所以还是不用的好.当然还有写一个委托来同步调用.但是我觉得它有点麻烦,就放弃用委托同步了.

后来我搞了好多种写法,都摆脱不了这个怨念,最终也摆脱了异常捕获并抛出了这个小框框.

框框上明确的说道:"线程间操作无效: 从不是创建控件 [控件名称]  的线程访问它。"但是在框框的Suggestion列表中显示出了一条项目,大意是:"如何:对 Windows 窗体控件进行线程安全调用.",我点开它,并且跳进了MSDN.

原来在MSDN上就有教咱如果夸线程操作控件啊.亏我还Google半天呢.

MSDN上的办法就是使用委托来同步调用控件的.具体方法我也就不卖关子了.因为MSDN上的代码比较繁多,所以我稍微改进了一下.

//首先声明一个委托.
delegate void SetValueDelegate(int val);

//模仿程序中的跨线程调用
public void Progress()
{
    Thread t = new Thread(() =>
        {
            //跨线程由此开始
            SetValue(100);
        });
    t.Start();
}

//关键部分在这里
public void SetValue(int val)
{
    //InvokeRequired属性当非创建本控件线程操作的时候
    //它返回true,此时使用窗体(该控件的创建者)的Invoke方法创建同步调用.
    //妙哉,妙哉!
    if (ProgressBar1.InvokeRequired)
    {
        var svd = new SetProgressValue(SetValue);
        this.Invoke(svd, val);
    }
    else
    {
        ProgressBar1.Value = val;
    }
}

好了,就是这样,非讨论文章就越精炼越好.希望能帮到你.

最后附上MSDN的地址:这里

29四/104

没有了Linq和扩展方法,一切都靠自己–手写Sort和Search方法.

众所周知在ASP.NET3.5中对于实现了IEnumerable<T>接口的类相比于ASP.NET2.0多出了好多方法出来,他们大部分都是基于Linq.在Linq的帮助下我们可以用1行代码就完成Sort和Search方法.

当然在ASP.NET 2.0 中 Array类中有一个静态Sort方法,还是List中也有一个Sort方法.但是他们分别都是独立并且接收的参数过于"小众",面对Linq关于IEnumerable接口实现的那些强大的Sort方法来说真是小巫见大巫啊.不但少了Func<T,TResult>委托,还少了Lambda表达式..昨晚搞一个实体类集合的排序就被郁闷到了.今早起床立即写了两个会用到的方法,当然相对于Linq来说还是比较小众,这是水平问题和语言本身的限制性.

他们分别是泛型类,并且都是静态方法,接收一个实现了ICompareable<T>接口的类型T.另外所用到的Sort方法来自最简单的QuickSort,而Search方法来自于限制比较大的BinarySearch(需要已经排序好的数组)和最最普通的遍历查找.最后他们都接收一个实现与IList<T>接口的类型作为排序或查找对象(已知Array和List都实现了这2个接口).

下面放上代码:

public class Sort<T> where T : IComparable<T>
{
    public static IList<T> QuickSort(IList<T> list, bool desc)
    {
        int lo = 0;
        int hi = list.Count - 1;
        int tag;
        if (lo < hi)
        {
            tag = QuickSortPartion(list, lo, hi, desc);
            QuickSort(list, lo, tag - 1, desc);
            QuickSort(list, tag + 1, hi, desc);
        }

        return list;
    }
    public static IList<T> QuickSort(IList<T> list, int lo, int hi, bool desc)
    {
        int tag;
        if (lo < hi)
        {
            tag = QuickSortPartion(list, lo, hi, desc);
            QuickSort(list, lo, tag - 1, desc);
            QuickSort(list, tag + 1, hi, desc);
        }

        return list;
    }
    private static int QuickSortPartion(IList<T> list, int lo, int hi, bool desc)
    {
        T pos = list[lo];
        T temp;

        if (!desc)
        {
            while (lo < hi)
            {
                while (lo < hi && pos.CompareTo(list[hi]) <= 0) hi--;
                temp = list[lo];
                list[lo] = list[hi];
                list[hi] = temp;
                while (lo < hi && pos.CompareTo(list[lo]) >= 0) lo++;
                temp = list[hi];
                list[hi] = list[lo];
                list[lo] = temp;
            }
        }
        else
        {
            while (lo < hi)
            {
                while (lo < hi && pos.CompareTo(list[hi]) >= 0) hi--;
                temp = list[lo];
                list[lo] = list[hi];
                list[hi] = temp;
                while (lo < hi && pos.CompareTo(list[lo]) <= 0) lo++;
                temp = list[hi];
                list[hi] = list[lo];
                list[lo] = temp;
            }
        }

        return lo;
    }
}

public class Search<T> where T : IComparable<T>
{
    public static int NormalSearch(IList<T> list, T item)
    {
        for (int i = 0; i < list.Count;i++ )
        {
            if (list[i].CompareTo(item) == 0)
            {
                return i;
            }
        }
        return -1;
    }

    public static int BinarySearch(IList<T> list, T item)
    {
        bool desc = isDescList(list);
        int lo = 0;
        int hi = list.Count - 1;
        int idx;
        while (lo <= hi)
        {
            idx = lo + (hi - lo) / 2;
            if (item.CompareTo(list[idx]) < 0)
            {
                if (!desc) hi = idx - 1;
                else lo = idx + 1;
            }
            else if (item.CompareTo(list[idx]) > 0)
            {
                if (!desc) lo = idx + 1;
                else hi = idx - 1;
            }
            else
                return idx;
        }

        return -1;
    }

    private static bool isDescList(IList<T> list)
    {
        int _base = 0;
        while (_base + 1 < list.Count && list[_base].CompareTo(list[_base + 1]) != 0)
        {
            if (list[_base].CompareTo(list[_base + 1]) > 0) return true;
            else return false;
        }

        return false;
    }
}

由于目前紧张的开发进度,所以写代码是目光比较短浅.
另外我看到了Array.Sort中有一个实现IComparer接口的参数,目前不清楚他是干嘛的,不过现在我马上要求会会他.或许可以让上面的代码更加美观,接受面更加广泛.

希望能帮到你 :-)

21四/101

老赵演讲:Why Java Sucks & C# Rocks 录音清晰版.

我个人是非常崇拜老赵的,可惜人不在上海无法现场光临他的关于这次语言层面上的演讲.

虽然这次演讲引起了部分Java程序员的反感,但是的确,从它讲这两种语言的发展史就能很清晰地感觉到C#语言的轻便性.

这次演讲的录音是他的朋友韦恩卑鄙提供的(话说我的英文名也是Wayne.呵呵).

我把它用牛顿3放大了下声音,并压缩一下波形,让老赵的声音更清晰平稳,这样听就不费劲,也不怕声音忽大忽小了.

边听录音边看PPT,效果灰常好..哈哈!

下载地址

31三/103

数据结构复习:用自己的方式实现List

最近复习起数据结构,真是后悔原来上课不好好听课.可以说当学校开设数据结构这门课程的时候虽然我知道他重要,但是我一直都在睡觉,而现在重新拿起这本书,我要好好把它看完,而这不叫复习了,叫做学习.

在书的第二章开始介绍数据结构的时候就提到了线性表,线性表理所当然的成为了数据结构中最简单的结构,而基础的线性表有2种,其一是顺序表,其二是链表.因为顺序表实在是太简单,简单到我们平常天天碰到的数组就是一个最典型的顺序表,而C#又提供了一组完美的数组操作方法,这样看来实现顺序表实在是没有什么挑战性(定义一个数组,搞2个方法就完事了).而链表不同,

链表是有N个包裹着实际数据的特殊类型的集合,而这些类型在内存中实际上又没什么联系,他们的关系在于这个特殊类的一个属性(Next)引用了下一个类型,以此类推,1的Next引用2,2的Next引用3..最终形成一个关系链,"链表"这个名字也由此而来.

废话不多说,我们就来实现一个泛型List, we call it MyList<T>

首先我们建立一个新的类,叫做MyList.cs,并且再建立一个上面所说的"特殊的类"代码如下:

namespace proj_0329
{
    /// <summary>
    /// Write a new Generic List called MyList.
    /// </summary>
    public class MyList<T>:IEnumerable<T>,IEnumerator<T>
    {
        //这就是上面所说的特殊类型,是个嵌套类,它没必要被其他任何类型访问到
        class MyListNode<t>
        {
            public MyListNode(t val, MyListNode<t> next)
            {
                this.val = val;
                this.next = next;
            }
            private t val;
            private MyListNode<t> next;
            public t Value { get { return val; } set { val = value; } }
            //本属性用于对下一个节点的引用.
            public MyListNode<t> Next { get { return next; } set { next = value; } }
        }
        //构造函数
        public MyList()
        {
            head = new MyListNode<T>(default(T), null);
            rear = null;
            length = 0;
            index = -1;
        }
        //私有变量
        //这是一个特殊的节点,它不包含值,它的index应该为-1,它的next才是MyList的第"零"个元素.
        private MyListNode<T> head;
        //此变量方便于添加新的节点.
        private MyListNode<T> rear;
        //MyList的总长度
        private int length;
        //用于IEnumerator接口的Current属性,返回当前被访问到第几个元素.因为一下的实现,所以初始值为-1.
        private int index;

        public int Length { get { return length; } }

        //索引器,访问元素方便
        public T this[int i]
        {
            get
            {
                return Seek(i);
            }
        }

        //添加元�
        public void Add(T item)
        {
            MyListNode<T> node = new MyListNode<T>(item, null);
            if (length == 0)//当length==0,我们要操作特殊的head节点.
            {
                head.Next = node;
                rear = node;
                //他们2个应该都指向现在被添加的第一项.
            }
            else
            {
                rear.Next = node;//把尾部的节点的Next属性设置为当前要插入的属性
                rear = node;//再把尾部节点设置为当前对象.
            }
            length++;
        }

        public void Remove(int i)
        {
            if (i > length || i < 0)
                return;
            if (i > 0)
            {
                MyListNode<T> prev = SeekNode(i - 1);//找到当前要操作节点的前趋.
                MyListNode<T> cur = prev.Next;//获取当前节点.
                prev.Next = cur.Next;//把前节点的Next属性设置为当前节点的Next属性,也就是说当前节点被架空,失去引用的它将被GC处理.
                cur = null;//让当前对象彻底从内存里消失吧!
            }
            else
            {
                //又要考虑head节点
                MyListNode<T> tmpVal = head.Next;//获取head节点的下一个节点,也就是MyList的第"零"个元素的下一节点.
                head.Next = tmpVal.Next;//再把head的下一节点设置为第"零"个元素的下一节点.此时第"零"个元素被架空,失去引用.
                tmpVal = null;
            }
            length--;
        }

        public T Seek(int i)
        {
            return SeekNode(i).Value;
        }

        //私有方法,为方便获取节点,索引器和Seek只要返回当前节点的值就完成工作.
        private MyListNode<T> SeekNode(int i)
        {
            int j = 0;
            MyListNode<T> tmpNode = head.Next;
            if (i < 0 || i > Length)
                throw new ArgumentOutOfRangeException();
            if (tmpNode == null)
                return null;
            while (j < i && j < Length)
            {
                if (tmpNode.Next != null)
                {
                    tmpNode = tmpNode.Next;
                    j++;
                }
                else
                    break;
            }
            if (j == i)
                return tmpNode;
            else
                return null;
        }
        //余下的都是完成接口的实现,这就没什么好说的了,毕竟foreach还是比较顺手的循环方式.

        #region IEnumerable<T> 成员

        public IEnumerator<T> GetEnumerator()
        {
            return this as IEnumerator<T>;
        }

        #endregion

        #region IEnumerable 成员

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this as IEnumerator;
        }

        #endregion

        #region IEnumerator<T> 成员

        public T Current
        {
            get { return this[index]; }
        }

        #endregion

        #region IDisposable 成员

        public void Dispose()
        {
            head.Next = null;
            GC.SuppressFinalize(this);
        }

        #endregion

        #region IEnumerator 成员

        object System.Collections.IEnumerator.Current
        {
            get { return this[index]; }
        }

        public bool MoveNext()
        {
            index++;
            return index < Length;
        }

        public void Reset()
        {
            index = -1;
        }

        #endregion
    }
}

认清机制:
从以上的代码可以看出我们并没有一个绝对的容器来装放这些MyListNodes,也就是说我们根本不能像一个数组来那样直观得可以知道到底是那个变量里装着我们这些链表数据,我们的代码里只能获取到链表的头和尾.此时的MyList并不认识我们链表的第n(n不是链表头或者尾)个元素,可以说当我们建立完链表之后就像放羊一样地让这些"孩子"自由流浪,但前提是排名第n的"孩子"一定要给排名第n-1的"孩子"留下联系电话,至于这"孩子"将来要到哪里去,要定身于天涯还是海角,我们并不管他.这样就可以动态得管理这些元素所占用的空间,世界无限大(指内存),我们也就可以随意的添加和删除元素,只要它还没满到装不下一个新的元素为止.相对于顺序表的预先分配空间,就好象母亲买了一个N室1厅的房子,一切尽在掌握中,当孩子住满这些房间,也就不能再添加元素了.

另外既然这些孩子散落在世界各地,而母亲一时间也不知道孩子们究竟在哪里,那这些孩子们会不会最终被垃圾回收机制给回收?答案是否定的.GC定义只要一个在当前程序中的任何数据,只有当它失去了所有的引用才会被回收,而链表的第n个孩子总是还有一个最亲的亲人:n-1,n-1知道n的电话号码,妈妈就不会失去与n的联系,GC也不会将黑手伸向n.

链表真是一个有意思的数据结构阿.

性能考虑:

根据以上的说法我们可以非常清楚得认清链表与顺序表的性能差别.对于顺序表,试想一下如果妈妈叫孩子们吃饭的情景,因为孩子们都在妈妈的掌控之中,每个人的手机号码她都有,即便孩子出门溜达也能立刻喊她们回来吃饭.

链表就比较悲剧,如果我们要叫第n个孩子回家半点事,母亲就得从"head"孩子那儿要到排名老1的孩子的电话号码,再像老1要老2的号码,以此类推,直到母亲拿到了第n个孩子的电话号码为止,而这小子此时可能还在国外,这时把它从国外叫回身边可又要花不少时间.

还好在电子世界中"孩子"们的寿命可能不到1秒钟,他们的办事速度(指电脑的运行速度)比人的办事速度快了多了去了,以上的事情在我们不自觉的情况下飞速的运作着.

最后根据以上的算法,如果我们要获取最后一个元素是不是要把链表整个遍历一遍?答案是肯定的.但是我们可以给MyListNode添加前趋元素的引用,并且通过更好的优化算法来获取更快的访问速度.

祝各个ASP.NET程序员都能很好的认清.net框架实现,学好数据结构和一些基本的算法,彻底.NET平台"慢"的劣势!

27三/100

Let’s Rock! 第一季-用ASP.NET扫雷!(3)

上回讲述了Rule这个最重要类的一部分关于游戏初始化的方法.而之前给出的Click方法中可以看出在鼠标左键或右键双击时会执行2个方法,分别为dig(挖)和flag(标记).那么我们开来看看这两个方法里有什么乾坤.

Dig(Block b)

我们获取到我们鼠标单击时的坐标,我们可以判断出我们点击了哪一个块.经过Click方法的分析呢,将这个获取到的块传递到Dig方法中.这时我们就得开始考虑b的一切可能性.如果b.IsMine==true的话,意味着我们的游戏就结束了.所以我们在判断完b.IsMine是否为true之后将执行一个GameOver方法.如果b.IsFlag的属性为true的话,我们将不做任何事情,直接return.余下就是判断它的b.IsDig的属性是否为true,如果为true,执行一段最容易出错的代码,如果为false,将它的IsDig属性设置为true,就代表这个块被挖开了,并且没被雷给炸到.

那么上面所说的那段最容易出错的代码是什么呢?

我们接下来思考一下我们点开一个已经翻开的块时,游戏要怎么做才让我们满意?—显然是展开它周围8个块中尚未翻开的的那一部分.

通常懂得玩扫雷游戏的玩家都知道当我们确定这个已翻开的块的周围没有雷的情况下我们才会点击它,并且命令它展开,那么此时有2种情况:

  1. 当前块标记的是周围有X个雷,但玩家并未给它标记慢X个旗--此时游戏应该丢弃玩家这次点击的请求,以示警告.
  2. 玩家给当前块标记满了X个或X个以上的旗子—此时游戏将会听话地将其周围的块都展开,无论玩家是否标记错误.如果标记错误,不好意思,Game Over.至于多标记的块,我们就当作一个隐患埋在游戏当中,并不挖开(也不能挖开,Dig方法碰到已经被标记的块会跳出方法块,什么事也不干的)谁叫玩家自己不小心的.

好了.现在我们健壮的Block类终于有用武之地啦!具体就不再多说,相信您已经被我给绕晕了.

没关系,代码来啦.


private static void Dig(Block b)

{

    if (b.IsFlag)

    {

        return;

    }

    if (b.IsDig)

    {

        if (b.MineSurround == 0)

        {

            foreach (var ob in b.Surrounds)

            {

                if (!ob.IsDig) Dig(ob);

            }

        }

        else

        {

            if (b.FlagSurround >= b.MineSurround)

            {

                foreach (var ob in b.Surrounds)

                {

                    if (!ob.IsDig) Dig(ob);

                }

            }

        }

    }

    else if (!b.IsDig && b.IsMine)

    {

        Runtime.GameOver(false);

    }

    else

    {

        b.IsDig = true;

        if (b.MineSurround == 0)

            Dig(b, true);

    }

    CheckAfterDig();

}

大家可以看到当判断b.IsDig的时候,我们的代码先判断了b周围的雷的个数,如果周围雷的个数为0,游戏将自动帮我们展开它周围的所有未展开项.而展开的方法还是Dig,也就是说游戏将进入一个Dig方法的循环当中.所以我们应当避免Dig方法永远的挖下去,再挖就内存溢出了(显然控制这段代码不进入死循环有点困难,我尝试了好多次,最终以最完美的姿态展示给大家.)!如果b周围雷的个数超过0个,那么游戏将不展开所有的项,那么方法执行到这里也就停了,也就控制了它进入死循环.

Flag(Block b)

标记的语法就没什么好说的代码也非常简单,不具备什么逻辑性.


private static void Flag(Block b)

{

    if (!b.IsDig && !b.IsFlag)

    {

        b.IsFlag = true;

        Runtime.GameInfo.Flags++;

    }

    else if (!b.IsDig && b.IsFlag)

    {

        b.IsFlag = false;

        Runtime.GameInfo.Flags--;

    }

}

好了,最近的时间不多,所以每一章节放的内容也不多,4月份就会更加的忙,希望我能忙中偷闲,写下一篇新的博文

18三/100

浅测Try&Catch的性能到底有多差(3)

上2节咱们使用30000行杨辉三角来测试try&catch的性能,结果差强人意.原因是什么我不是很确定,但是有一点我觉得有点问题,而且结果很有可能就差在这上面:我们无法确定try块被执行了多少次,catch块被执行了多少次.当然算是能算,就是太麻烦.而这次我把算法设定的很简单.代码如下:

using System;
using System.Diagnostics;

public class MyClass
{
	public static void Main()
	{
		string[] strs=
		{
			"I'm",
			"Snake"
		};
		int rounds=1000000;
		Stopwatch sw=new Stopwatch();
		for(int r=0;r<5;r++)
		{
			sw.Reset();
			sw.Start();
			for(int i=0;i<rounds;i++)
			{
				string s=strs[i%2];

				if(s.Length>5)
					s.Substring(3,1);
				else
					s.Substring(0,1);
				/*
				try
				{
					s.Substring(3,1);
				}
				catch
				{
					s.Substring(0,1);
				}
				*/
			}
			sw.Stop();
			Console.WriteLine(sw.ElapsedMilliseconds);
		}
		Console.ReadLine();
}
}

这个算法优点是不占内存,另外使用了引用类型的简单操作.,并且我们可以知道算法中各个块被执行了多少次.
这次我毫不留情地把基数(即:rounds)设定得很大,达到一百万.那么我们的if&else还有块在这个算法中执行了多少次呢?

循环次数:1,000,000
if块: 1,000,000次
else块 500,000次
try块 1,000,000次
catch块 500,000次

好的,让我们来看看运行结果吧!

If&Else
1. 40ms
2. 39ms
3. 37ms
4. 37ms
5. 40ms
6. 38ms
7. 37ms
8. 37ms
9. 37ms
10. 39ms
平均值:38.1 ms

Try&Catch
1. 39947ms
2. 39744ms
3. 39054ms
4. 39971ms
5. 40006ms
然后我实在等不下去了…于是中止了10次循环.
好吧,性能差了整整1000倍.
这次再改改代码,事先说一下,这次改代码是出于个人兴趣,比较一下引用类型和值类型的性能,跟本主题无关.
更改代码如下:

int[] ints={0,1};
int rounds=1000000;
for(int i=0;i<rounds;i++)
{
	int n=ints[i%2];

	if(n>0)
		n = n/2;
	else
		n = n+1/2;
}

运行结果是清一色的1ms.感叹一下值类型的操作果然比引用类型快,那么题外话就赶快结束吧.

回到我们之前操作string的代码中,我们这次的代码修改如下:

public class MyClass
{
	public static void Main()
	{
		string str="Hi";
		int rounds=1000000;
		Stopwatch sw=new Stopwatch();
		for(int r=0;r<10;r++)
		{
			sw.Reset();
			sw.Start();
			for(int i=0;i<rounds;i++)
			{

				if(str.Length>5)
					str.Substring(3,1);
				else
					str.Substring(0,1);
				/*
				try
				{
					s.Substring(3,1);
				}
				catch
				{
					s.Substring(0,1);
				}
				*/
			}
			sw.Stop();
			Console.WriteLine(sw.ElapsedMilliseconds);
		}
		Console.ReadLine();
	}
}

这次的执行次数如下:
循环次数:1,000,000
if块: 1,000,000次
else块 1,000,000次
try块 1,000,000次
catch块 1,000,000次

运行结果如下:

if&else运行10次结果如下:
1. 37 ms
2. 36 ms
3. 37 ms
4. 36 ms
5. 36 ms
6. 36 ms
7. 36 ms
8. 40 ms
9. 36 ms
10. 36 ms
见鬼,比之前的还快= =

那么轮到测试try&catch块了:
1. 85498ms
2. 84335ms
我不想再测试了.这次性能更可怕了.原来try块跟catch块都有性能消耗.那finally块呢?我们来验证一下吧!
加上finally以后,耗时还是85ms左右.那就不让catch块执行看看执行时间有什么变化呢.
结果竟然和if&else一样的37ms!

本节总结:

根据上面的分析,特别是文章末尾的分析非常明显的表明了如果在try块中执行无误的代码的性能损失几乎为0,但是一旦try块中的代码出现错误,那么CLR(应该是这玩意)就会生成一个异常,并且进入catch块中.catch块中搞了什么鬼我不知道,也暂时没能力知道,但是最终倒是让我明白了原来罪魁祸首是出了错的try块中的代码导致运行catch才出现的性能损耗.
最后我要说个前两节都忘记说的事:我是用Snippet Compiler编写并执行所有的代码的,而据说Snippet Compiler是以debug的形式执行的代码.所以所有之前的代码性能可能会被debug模式影响到.但具体影响的大不大就请高手告知我一声,而完成了这篇代码之后我还有其他正式得干呢,呵呵.

   下一页