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

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模式影响到.但具体影响的大不大就请高手告知我一声,而完成了这篇代码之后我还有其他正式得干呢,呵呵.

17三/100

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

昨晚因为快要熄灯断电的缘故,发表上一篇文章有些仓促.在昨晚熄灯之后我考虑到算法可以简化,于是今早放学之后就马上回来改了改算法.代码如下:

public class MyClass
{
	public static void Main()
	{
		int maxLine=30000;
		int[][] ns=new int[maxLine][];
		Stopwatch sw=new Stopwatch();
		sw.Reset();
		sw.Start();
		for(int i=0;i<maxLine;i++)
		{
			ns[i]=new int[i+1];
			ns[i][0]=1;
			for(int j=1;j<=i;j++)
			{

				if(j==i)
				{
					ns[i][j]=1;
					continue;
				}
				else
				{
					ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
				}
			/*
				try
				{
					ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
				}
				catch(Exception ex)
				{
					ns[i][j]=1;
				}
			}
			*/
		}
		sw.Stop();
		Console.WriteLine(sw.Elapsed.ToString());
		Console.ReadLine();
	}

}

最后执行10000行的时间缩短了0.1~0.15秒.但这显然不是我现在要阐述的论题.
在上一篇文章我并未提出我的平台和操作系统,这显然是对测试的一种不尊重,所以赶紧放上我的测试环境详情:
操作系统:windows 7 Ultimate
主板芯片组: A780G(超级缩水版)
CPU:AMD X2 2700MHZ(5000+黑盒)
内存:4G(金士顿 ddr2 800 2G*2)
还好我没有高估我的内存.刚才想提高测试的函数,于是在10000后面加了个零,差点没把我电脑给搞歇菜了.于是只再加了20000行.那么现在在改进了算法和行数之后他们的效率又会是如何呢?
ps:这算法明显给if和else减少压力从原来的四段变为两段,而try&catch负担依旧很重.另外我就不截图了,截图太麻烦,而且我之准备让程序在运行期间只执行2次,并不是原先的5次,可能是程序内存占用的原因吧,每次运行时间都相继减少,而且内存也吃不消,30000行1趟占用2G左右的内存.

下面是if&else方式执行30000行的记录:

1.1    30000lines     2.956s

1.2    30000lines    3.751s

2.1    30000lines    2.826s

2.2   30000lines    3.619s

3.1    30000lines    3.068s

3.2   30000lines    3.306s

现在是try&catch方式执行30000行的记录:

1.1    30000lines    7.611s

1.2    30000lines    7.813s

2.1    30000lines    6.804s

2.2    30000lines    7.064s

3.1    30000lines    6.945s

3.2    30000lines    7.036s

这次虽然算法偏向了if&else,但是最终的结果并没有跟try&catch拉开更大的距离,反而缩小了一倍.

在执行30000行测试的时候发现内存占用特别厉害,将近100%,可能是在这方面遇上瓶颈,于是我缩减10000行代码再试试看.

1.1    20000lines     1.430s

1.2    20000lines    1.755s

2.1    20000lines    1.404s

2.2   20000lines    1.851s

3.1    20000lines    1.410s

3.2   20000lines    1.764s

现在是try&catch方式执行30000行的记录:

1.1    20000lines    3.536s

1.2    20000lines    4.092s

2.1    20000lines   3.613s

2.2    20000lines   4.103s

3.1    20000lines    3.558s

3.2    20000lines    4.080s

我郁闷了.不过也没关系,事实已经证明了try&catch是性能低下的东西.

本节总结:

本节使用杨辉三角的优化(较前一节)算法,并且提升了运算量,但结果if&else的优势反而变小了,先不管原因是什么,以后您还会在代码中大量的"踹"一下再"咔吃"一下吗?事实上很多异常都可以提前判断,特别是"引用类型是否为空"和指定格式"是否为预期类型"的判断基本上不需要使用try&catch块,特别在大量的循环中.

try&catch的确给我们带来方便,但它的性能也的确难以恭维,另外也降低了我们原有的编写代码的谨慎程度.所以在我个人编写的代码中一般是不会出现try&catch的,除非必要.

好了,本节也正式完成,但是因为杨辉三角的复杂度导致程序运算中try&catch发生的概率难以确定(确定是肯定可以,但是我实在懒得去验证,很久没有拿笔在草纸上计算数学问题了).那么下一节我将以一个简单算法来验证if&else和try&catch之间的性能,我们至少要确定一次循环内至少触发了多少次try和catch,或执行了多少次判断才能更精确地确定这两者的性能.那么如果您还有兴趣看下去的话,咱们就在下一篇"不见不散" : )

16三/100

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

今天中午本来是java课老师叫我们用它来实现杨辉三角.当初已经临近下课,由于用不惯Eclipse,一直卡在一个下标越界的错误上,直到下课也没把程序正常运行出来.晚上回宿舍就特别不爽,于是用C#将之重写了一遍,首次测试运行,结果成功.心理稍稍有点欣喜.

由于未来的就业压力,我脑子突然一热,再想未来面试途中如果面试官刚好叫我输出杨辉三角我应该怎么写它比较快,切不容易错呢?在我的办法呈现之前,先把我的算法代码贴上来(由于本人数学的那根筋比较细,算法不一定是最佳算法)

public class MyClass
{
	public static void Main()
	{
		int maxLine=8;
		int[][] ns=new int[maxLine][];
		for(int i=0;i<maxLine;i++)
		{
			ns[i]=new int[i+1];
			for(int j=0;j<i+1;j++)
			{
				if(i-1<0)
				{
					ns[i][j]=1;
					continue;
				}
				else if(j - 1 < 0)
				{
					ns[i][j]=1;
					continue;
				}
				else if(ns[i-1].Length<j+1)
				{
					ns[i][j]=1;
					continue;
				}
				else
				{
					ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
				}
			}
		}

		for(int i=0;i<MaxLine;i++)
		{
			for(int j=0;j<i+1;j++)
			{
			    int t=ns[i][j];
				if(t<10)
					Console.Write(" ");
				Console.Write(t.ToString()+" ");
			}
			Console.WriteLine("");
		}
		Console.ReadLine();
	}
}

于是结果如下:

是不是相当标准 : )
好了,由于前面的代码有一长串if和else语句,个人认为这个代码丑了点,于是决定直接不判断,直接try和catch一下.于是上面的代码发生的变化:

for(int i=0;i<maxLine;i++)
{
        try
        {
            ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
        }
        catch
	{
	    ns[i][j]=1;
	}
}

现在代码清爽了点.但是由于以前听说过try&catch的性能相当的差,于是谨慎得测试一下性能会比较好,而且到头来还能多长点见识.于是改写代码如下(这时就不输出结果了,只是纯粹后台操作):

public class MyClass
{
	public static void Main()
	{
		int maxLine=10000;
		int[][] ns=new int[maxLine][];
		Stopwatch sw=new Stopwatch();
		for(int t=0;t<5;t++)
		{
			sw.Reset();
			sw.Start();
			for(int i=0;i<maxLine;i++)
			{
				ns[i]=new int[i+1];
				for(int j=0;j<i+1;j++)
				{
					if(i-1<0)
					{
						ns[i][j]=1;
						continue;
					}
					else if(j - 1 < 0)
					{
						ns[i][j]=1;
						continue;
					}
					else if(ns[i-1].Length<j+1)
					{
						ns[i][j]=1;
						continue;
					}
					else
					{
						ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
					}
//					try
//					{
//						ns[i][j]=ns[i-1][j-1]+ns[i-1][j];
//					}
//					catch(Exception ex)
//					{
//						ns[i][j]=1;
//					}
				}
			}
			sw.Stop();
			Console.WriteLine(sw.Elapsed.ToString());
		}
		Console.ReadLine();
	}
}

我先是测试了一下多次if和else的10000行操作:
结果我已经截图下来了:总共10次.


10000万行的总共用时大概在0.5秒左右.而这次我要运行try&catch块的代码:


这个结果果然验证了try&catch性能差的命题了!相差3~4倍的时间!
所以如果您下次要在超级循环中用到try&catch的时候,该谨慎点了.

14三/100

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

上一部分的内容介绍了扫雷的运行时配置,那么这一节我们需要编写游戏规则算法来操作运行时的配置.
首先我们得从游戏的初始化开始.游戏刚启动时,运行时配置中的Blocks[]一定是个null值,也就是说它还未初始化.那么我们就要为游戏写一个初始化函数.那么我们就要将前面的Config这个运行时配置类写一个构造函数.让它在初始化的时候将一些必要的成员给赋上值.

public Config(int blocksPerRow, int rowCount, int mineCount, string levelTitle, Control container)
        {
            MineCount = mineCount;
            BlocksPerRow = blocksPerRow;
            RowCount = rowCount;
            LevelTitle = levelTitle;
            Flags = 0;
            TimeStarted = DateTime.Now;

            container.Controls.Clear();
            Panel = new SuperPanel()
            {
                Width = this.Width,
                Height = this.Height,
                Name = "SuperPanel"
            };
            if (Panel.Width < container.Width)
            {
                Panel.Left = (container.Width - Panel.Width) / 2;
            }
            if (Panel.Height < container.Height)
            {
                Panel.Top = (container.Height - Panel.Height) / 2;
            }
            container.Controls.Add(Panel);
        }

在这个构造函数的前半部分,我们将那些必须的成员都已经初始化了我们想要的值,而后半部分就是实例化一个新的SuperPanel,并且调整了这个SuperPanel的位置,使它居中.最后在container中最佳这个Panel.
该构造函数的前四个参数应该很好理解,最后一个类型为Control的container到底是什么呢?因为我们的游戏中一定会有一块地方用来描绘我们扫雷所用到的”块”,那么这个地方我们将来会用一个Panel来规范它的位置及其大小.
可能有人会问Control和Panel是不一样的类型啊,为什么你要用Control类型而用Panel类型呢?首先winform中的所有空间均继承自Control类,而C#语言认为子类转换为父类是类型安全的行为;其次,如果我们哪一天用了不想用Panel,而改为用SplitControl或者GroupBox这些控件,那么我们最终还要回来修改这个函数;最后,在构造函数中我们只需要用到Control类的成员,如Add和Clear方法,Height和Width属性等.这是所有控件都有的成员(因为它们都继承自Control嘛..),所以我们用Control这个宽松的入口用来适应我们游戏未来需求的变更.
另外这个SuperPanel是啥?其实是我自行写的一个继承自Panel的类.它的实现非常简单,首先创建一个类,使他继承自Panel,最后在构造函数中添加几行代码,最终结果是这样:

public class SuperPanel:Panel
{
    public SuperPanel()
    {
        this.SetStyle(
ControlStyles.OptimizedDoubleBuffer|
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint, true
);
        this.DoubleBuffered = true;
        this.UpdateStyles();
}
}

这是开启控件的双缓冲,解决控件在描绘画布时候闪烁的问题.具体什么是双缓冲就有请大家”百度一下”了.

终于完成了Config类的编写,现在就要开始编写一下运行时这个类了.运行时类里除了包含运行时配置以外,还得包含其他东西,这些函数我们现在暂时无法预知,所以我们就不要在运行时中添加其他未来可能用不到的东西.
新建一个类:Runtime.在游戏中我们的”运行时”是必须唯一且不能创建多个版本的,否则在游戏运行的时候我们的规则访问这些数据的时候乱套,不知道该访问那个运行时配置.所以我们在Runtime中的所有成员都必须标记为”static”.

public static class Runtime
{
public static void Initilaize() { IsInitialized = true; }
    public static bool IsInitialized { get; private set; }

public static Config GameInfo
    {
        get
        {
            if (_gameInfo == null)
                throw new NullReferenceException("游戏尚未初始化!");
            return _gameInfo;
        }
    }
    private static Config _gameInfo;
}

完成”运行时”这个数据中心之后,我们就可以开始创建规则算法了.还是老样子,新建一个新的类,名为”Rules”,跟上面一样,配置只能有一个,而规则也只能有一个,所以在游戏中我们也没必要创建多个版本的Rules来对运行时配置进行操作.所以我们将Rules内的方法全部设定为静态方法.

Ps:上一节我们的Blocks是一维数组,当然使用二维数组更为方便,由于多种原因我在接近完成扫雷这一游戏的时候,因为想让计算方法更快,引用了”图”这一数据结构,把二维数组改为一维数组.但是最后放弃了这个方法,感觉上有点多此一举,而后来也懒得把一维数组改回二维的了,反正也没麻烦到哪里去.

给运行时配置中的Blocks初始化.没什么好说的..

public class Rules
{
    public static void GenBlocks()
    {
        Runtime.GameInfo.Blocks = new Block[Runtime.GameInfo.RowCount * Runtime.GameInfo.BlocksPerRow];
        int bCount = Runtime.GameInfo.Blocks.Length;
        for (int i = 0; i < bCount; i++)
        {
            Runtime.GameInfo.Blocks[i] = new Block()
            {
                Index = i,
                IsFlag = false,
                IsDig = false,
                IsMine = false,
                X = i % Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.BlockSize,
                Y = i / Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.BlockSize
            };
        }
        GenMines();
    }
}

结尾部分出现了GenMines这个方法,这是给Block[]中添加地雷,这一布我放到初始化结束之后来做就方便很多:

private static void GenMines()
{
    Random r = new Random(DateTime.UtcNow.Millisecond);
    int mineCount = 0;
    while (mineCount < Runtime.GameInfo.MineCount)
    {
        int x = r.Next(0, Runtime.GameInfo.BlocksPerRow * Runtime.GameInfo.RowCount);
        if (!Runtime.GameInfo.Blocks[x].IsMine)
        {
            Runtime.GameInfo.Blocks[x].IsMine = true;
            mineCount++;
        }
    }
}

首先我们创建一个Random实例用来生成随机数,用while循环和运行时配置中某一块是否是雷判别来保证生成足够多并且不重复的雷.多亏我们前面给Block设定了多个Is某某某的属性,我们只要设定块的isMine属性为true就能让它从一个无辜的方块变为恐怖的炸弹.

初始化游戏之后我们要进行鼠标点击的操作.传统的扫雷左键单击为挖方块,右键为标记为雷或不确定是不是雷,左右键一起按则是打开这一块周围所有的块.
我们就来个简化,左键依然是打开方块,但如果当前块已经被打开则打开它周围所有的块,右键仅标记当前块为雷.左右键就扔掉它.因为它实现起来不方便,我目前想到的方法是定义timer在鼠标某一个键按下的xx毫秒之内继续按下另一个键则触发左右键模式.

鼠标单击事件处理函数:

public static void Click(MouseEventArgs e)
{
    if (!Runtime.IsGameOver)
    {
        int x = e.X / Runtime.GameInfo.BlockSize;
        int y = e.Y / Runtime.GameInfo.BlockSize * Runtime.GameInfo.BlocksPerRow;
        var btn = e.Button;
        Block b = Runtime.GameInfo.Blocks[y + x];
        if (OnClick != null)
            OnClick(b);
        switch (btn)
        {
            case MouseButtons.Left:
                Dig(b);
                break;
            case MouseButtons.Right:
                Flag(b);
                break;
            default:
                break;
        }
        Runtime.GameInfo.Panel.Invalidate();
        if (Clicked != null)
            Clicked(b);
    }
}

上面的代码应该很清楚,因为这些块是画上去的,而不是windows的控件,所以我们要在运行是配置中的panel的鼠标单击事件中提取鼠标的X,Y值进行计算分析得到鼠标点击所对应的块.
然后触发OnClick事件,鉴于这是玩家与游戏交互的最重要的操作,未来一定有不少操作要在Click事件的执行的时候同时执行.于是定义了Onclick和Clicked这两个事件.这两个事件的定义代码如下:

public delegate void ClickHandler(Block b);
public static event ClickHandler OnClick;
public static event ClickHandler Clicked;

事件完美地解决了编程中的强依赖关系,这个方法在执行的时候不知道会有谁对它有兴趣,即便这个方法知道,那么他的代码里就要添加一条对方的响应函数.如果某一天一个新的类对它也有兴趣怎么办?那我们还要回来在这个方法里再添加一行这个新类的响应函数.一个新类还好,三天两头的出新的类,并且这些类都对于前面那个方法有兴趣,那我们这么改也不是办法.引进事件之后,我们就关闭了这个方法的修改,并开放了这个类的扩展.谁要对它感兴趣,就订阅它的事件,当事件一旦被执行,就会通知所有的订阅者执行响应函数.对事件还不大了解的朋友们可以到张子阳的博客上看看他关于委托和事件的详细讲解.

好了,这一回就讲到这里.下一节继续讲我们的规则算法和处理函数.咱们下回再见: )

12三/102

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

我决定做个专题,专门用ASP.NET搞点比较酷的东西,但前提是面向初学者就叫他”Let’s Rock”吧.但毕竟本人的修为并不可以做到一览众山小的境界.首先来个最最普通的游戏—扫雷.一说扫雷可能部分人已经失去兴趣了.其实一个游戏,除了给定的规则以外,如何实现它可以说方法无穷多的,那么我在未来的开发过程中会遵循一下的原则(但可适当打破):
1. 使用.NET Framework 3.5框架开发.为什么, 我喜欢.net3.0的lambda表达式来简化许多操作,另外不去管Linq是否真正”已死”,在”Let’s Rock”系列中我们完全可以利用Linq的简便且快速的特性来保证项目的效率.
2. 尽量做到符合良好的面对对象特性.但首先我再OOP上也是属于菜鸟一个,之所以这么做就是为了在造福大家的时候造福下自己.让自己练习一下面对对象的编程方式.
那么废话不多说,咱们现在就开始吧!

游戏成品截图:

MineSweeper

1. 游戏规则
已经知道游戏规则的朋友可以跳过这一块.
扫雷的核心规则就是当你打开一个”块”的时候,如果这个块不是”地雷”,那么它会提示你离他最近且环绕在它周围的那些”块”中有几个是”地雷”.那么通过多个非”雷”的块我们就可以确定到底那些”块”是”地雷”.如果你运气不好踩到了”地雷”,游戏结束,但是你可以为这些自己认为是”雷”的”块”做上标记,防止自己忘记那些是一推测出的地雷.
正确的标记出所有的”地雷”或以挖开所有的非”地雷”块,则游戏结束.

2. 定义配置
了解了游戏的规则我们就可以把部分游戏运行时所必须的东西给确定下来.首先就是游戏的核心-“块”,我并不打算再写几个特殊的”块”(如”地雷块”和”标记块”)首先是没必要,其次是增添开发难度.
首先在创建一个解决方案,选择.net 3.5框架,并且选择windows分类下的windows应用程序.我们将来的扫雷毫无疑问将不会运行在WEB上.
重命名Form1.cs窗体,我们把它叫做Main吧.毕竟未来它是扫雷的主窗体.
建立一个类,名叫Block.cs,下面是定义Block的成员.

namespace Game_MineSweeper
{
    public class Block : IComparable<Block>
    {
        public Block() { }

        public int Index { get; set; }
        public bool IsMine { get; set; }
        public bool IsDig { get; set; }
        public bool IsFlag { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public int MineSurround
        {
            get
            {
                return Surrounds.Count(b => b.IsMine == true);
            }
        }
        public int FlagSurround
        {
            get
            {
                return Surrounds.Count(b => b.IsFlag == true);
            }
        }
        public int BlockSurround
        {
            get
            {
                return Surrounds.Count(b => b.IsFlag == false && b.IsDig == false);
            }
        }
        public List<Block> Surrounds
        {
            get
            {
                return _surrounds;
            }
        }
        public Image Pic
        {
            get
            {
                return null;
            }
        }

        private List<Block> _surrounds;

        #region IComparable<Block> 成员

        public int CompareTo(Block other)
        {
            return this.Index.CompareTo(other.Index);
        }

        #endregion
    }
}

这就是大体的定义,并且也给出了部分成员的实现.
Index就是Block的ID,它未来可能会用到在对比两个Block的时候确定他们是否是同一个Block,于是我引用了IComparable这个接口来实现对比的功能.
IsMine,IsDig,IsFlag分别是这个块的状态,为什么不把这状态合成成一个枚举类型的状态呢,首先是Block可以具有多种状态(如它可以是雷,与此同时它也同时被标记),如果合成成一个枚举类型就会出现MineNFlag的状态,并且在判断雷的状态时也比较麻烦.
X,Y当然是块显示时的坐标.
MineSurround,FlagSurround,BlockSurround分别是当前块四周的块中雷,标记,未开挖块的数量.这里就不多说了,未来我们会用到.
Surrounds并未完成它的实现.但是我们现在要明确它的功能:装载离当前块最近距离的所有块.因为它周围块的数目是游戏开始以后就决定的,所以我们使用一个私有的_surrounds变量来装放这些块的引用.而上面那些以Surround结尾的属性则在游戏当中是不固定的,所以我们就不需要用容器来盛放结果.
同样的Pic属性在游戏中也是不固定的.它是用来传递当前状态应有的图像给描绘画布的对象.
Compare方法就不用说了.

那么我们在运行时如何来组织并且存放这些Block呢?我们将会用到单件模式.
我们会创造一个类,类中放有的都是游戏运行时不变,但游戏运行之前不确定的一些内容.包括块的数量,每行几个块,每列几个块等等.

public class Config
    {
        public Config(int blocksPerRow, int rowCount, int mineCount, string levelTitle, Control container)
        {
            if (rowCount > 50)
                rowCount = 50;
            else if (rowCount < 5)
                rowCount = 5;
            if (blocksPerRow > 50)
                blocksPerRow = 50;
            else if (blocksPerRow < 5)
                blocksPerRow = 5;
            MineCount = mineCount < 10 ? 10 : mineCount;
            BlocksPerRow = blocksPerRow;
            RowCount = rowCount;
            LevelTitle = levelTitle;
            Flags = 0;
            TimeStarted = DateTime.Now;

            container.Controls.Clear();
            Panel = new SuperPanel()
            {
                Width = this.Width,
                Height = this.Height,
                Name = "SuperPanel"
            };
            container.Controls.Add(Panel);
        }
        private Config() { }

        public readonly int BlockSize = 32;
        public int MineCount { get; set; }
        public int BlocksPerRow { get; set; }
        public int RowCount { get; set; }
        public int Width { get { return BlocksPerRow * BlockSize; } }
        public int Height { get { return RowCount * BlockSize; } }
        public int BlockCount { get { return BlocksPerRow * RowCount; } }
        public int Flags { get; set; }
        public string LevelTitle { get; set; }
        public Block[] Blocks { get; set; }
        public DateTime TimeStarted { get; set; }

        public System.Windows.Forms.Control Panel { get; private set; }
}

BlockSize是不变的一个值,表明这些Block未来呈现的图像大小都是32*32像素.于是我现在已经在怀疑将它放在这里是不是合适.结论出来了,显然不合适.那么这就交给聪明的你来决定将它放在哪里比较好了.
MineCount显然是雷的数量,BlocksPerRow则是每行块的个数,RowCount是当前游戏有几行”块”. BlockCount是当前游戏Block的数量.Flags是当前游戏中已经被标记的块的个数.LevelTitle则是当前游戏的难度即便的名称.TimeStarted是游戏开始时的时间,用来计算玩扫雷总共花费的时间.
Width,Height是留给Panel决定它的高度和宽度,而Panel就是描绘这些块的画布啦!

好啦,讲到这里游戏大部分初始化工作就完成了.那么下一节我们就要开始写游戏的规则算法.