C#委托和事件(Delegate、Event、EventHandler、EventArgs)

14.1、委托

当要把方法作为实参传送给其他方法的形参时,形参需要使用委托。委托是一个类型,是一个函数指针类型,这个类型将该委托的实例化对象所能指向的函数的细节封装起来了,即规定了所能指向的函数的签名,也就是限制了所能指向的函数的参数和返回值。当实例化委托的时候,委托对象会指向某一个匹配的函数,实质就是将函数的地址赋值给了该委托的对象,然后就可以通过该委托对象来调用所指向的函数了。利用委托,程序员可以在委托对象中封装一个方法的引用,然后委托对象作为形参将被传给调用了被引用方法的代码,而不需要知道在编译时刻具体是哪个方法被调用。

一般的调用函数,我们都不会去使用委托,因为如果只是单纯的调用函数,使用委托更麻烦一些;但是如果想将函数作为实参,传递给某个函数的形参,那么形参就一定要使用委托来接收实参,一般使用方法是:在函数外面定义委托对象,并指向某个函数,再将这个对象赋值给函数的形参,形参也是该委托类型的对象变量,函数里面再通过形参来调用所指向的函数。

14.1.1、定义委托

语法如下:

delegate result-type Identifier ([parameters]);

说明:

result-type:返回值的类型,和方法的返回值类型一致

Identifier:委托的名称

parameters:参数,要引用的方法带的参数

小结:

当定义了委托之后,该委托的对象一定可以而且也只能指向该委托所限制的函数。即参数的个数、类型、顺序都要匹配,返回值的类型也要匹配。

因为定义委托相当于是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在一个类的内部定义,那么此时就要通过该类的类名来调用这个委托(委托必须是public、internal),也可以在任何类的外部定义,那么此时在命名空间中与类的级别是一样的。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:当委托定义在类的外面,那么可以加上public、internal修饰符;如果委托定义到类的内部,那么可以加上public、 private、 protected、internal。一般委托都是定义在类的外面的。

14.1.2、实例化委托

Identifier objectName = new Identifier( functionName )

实例化委托的实质就是将某个函数的地址赋值给委托对象。在这里:

Identifier :这个是委托名字。

objectName :委托的实例化对象。

functionName:是该委托对象所指向的函数的名字。对于这个函数名要特别注意:定义这个委托对象肯定是在类中定义的,那么如果所指向的函数也在该类中,不管该函数是静态还是非静态的,那么就直接写函数名字就可以了;如果函数是在别的类里面定义的public、internal,但是如果是静态,那么就直接用类名.函数名,如果是非静态的,那么就类的对象名.函数名,这个函数名与该对象是有关系的,比如如果函数中出现了this,表示的就是对当前对象的调用。

14.1.3、委托推断

C# 2.0用委托推断扩展了委托的语法。当我们需要定义委托对象并实例化委托的时候,就可以只传送函数的名称,即函数的地址:

Identifier objectName = functionName;

这里面的functionName与14.1.2节中实例化委托的functionName是一样的,没什么区别,满足上面的规则。

C#编译器创建的代码是一样的。编译器会用objectName检测需要的委托类型,因此会创建Identifier委托类型的一个实例,用functionName即方法的地址传送给Identifier的构造函数。

注意:

不能在functionName后面加括号和实参,然后把它传送给委托变量。调用方法一般会返回一个不能赋予委托变量的普通对象,除非这个方法返回的是一个匹配的委托对象。总之:只能把相匹配的方法的地址赋予委托变量。

委托推断可以在需要委托实例化的任何地方使用,就跟定义普通的委托对象是一样的。委托推断也可以用于事件,因为事件基于委托(参见本章后面的内容)。

14.1.4、匿名方法

到目前为止,要想使委托工作,方法必须已经存在。但实例化委托还有另外一种方式:即通过匿名方法。

用匿名方法定义委托的语法与前面的定义并没有区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:

using System;

namespace Wrox.ProCSharp.Delegates
{

    class Program
    {

        delegate string DelegateTest(string val);

        static void Main()
        {

            string mid = ", middle part,";

            //在方法中定义了方法

            DelegateTest anonDel = delegate(string param)
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

            Console.WriteLine(anonDel("Start of string"));

        }

    }

}
using System;

namespace Wrox.ProCSharp.Delegates
{

    class Program
    {

        delegate string DelegateTest(string val);

        static void Main()
        {

            string mid = ", middle part,";

            //Lamada表示法

            DelegateTest anonDel = ( param)=>
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

            Console.WriteLine(anonDel("Start of string"));

        }

    }

}

委托DelegateTest在类Program中定义,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:它前面是关键字delegate,后面是一个参数:

            DelegateTest anonDel = delegate(string param)
            {

                param += mid;

                param += " and this was added to the string.";

                return param;

            };

匿名方法的优点是减少了要编写的代码。方法仅在有委托使用时才定义。在为事件定义委托时,这是非常显然的。(本章后面探讨事件。)这有助于降低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行得不太快。编译器仍定义了一个方法,该方法只有一个自动指定的名称,我们不需要知道这个名称。

在使用匿名方法时,必须遵循两个规则:

1、在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

2、在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量。方法内部的变量、方法的参数可以任意的使用。

如果需要用匿名方法多次编写同一个功能,就不要使用匿名方法。而编写一个指定的方法比较好,因为该方法只需编写一次,以后可通过名称引用它。

14.1.5、多播委托

前面使用的每个委托都只包含一个方法调用,调用委托的次数与调用方法的次数相同,如果要调用多个方法,就需要多次给委托赋值,然后调用这个委托。

委托也可以包含多个方法,这时候要向委托对象中添加多个方法,这种委托称为多播委托,多播委托有一个方法列表,如果调用多播委托,就可以连续调用多个方法,即先执行某一个方法,等该方法执行完成之后再执行另外一个方法,这些方法的参数都是一样的,这些方法的执行是在一个线程中执行的,而不是每个方法都是一个线程,最终将执行完成所有的方法。

如果使用多播委托,就应注意对同一个委托调用方法链的顺序并未正式定义,调用顺序是不确定的,不一定是按照添加方法的顺序来调用方法,因此应避免编写依赖于以特定顺序调用方法的代码。如果要想确定顺序,那么只能是单播委托,调用委托的次数与调用方法的次数相同。

多播委托的各个方法签名最好是返回void;否则,就只能得到委托最后调用的一个方法的结果,而最后调用哪个方法是无法确定的。

多播委托的每一个方法都要与委托所限定的方法的返回值、参数匹配,否则就会有错误。

我自己写代码测试,测试的结果目前都是调用顺序和加入委托的顺序相同的,但是不排除有不同的时候。

delegate result-type Identifier ([parameters]);

14.1.5.1、委托运算符 =

Identifier objectName = new Identifier( functionName);

或者

Identifier objectName = functionName;

这里的“=”号表示清空 objectName 的方法列表,然后将 functionName 加入到 objectName 的方法列表中。

14.1.5.2、委托运算符 +=

objectName += new Identifier( functionName1);

或者

objectName += functionName1;

这里的“+=”号表示在原有的方法列表不变的情况下,将 functionName1 加入到 objectName 的方法列表中。可以在方法列表中加上多个相同的方法,执行的时候也会执行完所有的函数,哪怕有相同的,就会多次执行同一个方法。

注意:objectName 必须是已经赋值了的,否则在定义的时候直接使用该符号:

Identifier objectName += new Identifier( functionName1);或者

Identifier objectName += functionName1;就会报错。

14.1.5.3、委托运算符 -=

objectName -= new Identifier(functionName1);

或者

objectName -= functionName1;

这里的“-=”号表示在 objectName 的方法列表中减去一个functionName1。可以在方法列表中多次减去相同的方法,减一次只会减一个方法,如果列表中无此方法,那么减就没有意义,对原有列表无影响,也不会报错。

注意:objectName 必须是已经赋值了的,否则在定义的时候直接使用该符号:

Identifier objectName -= new Identifier( functionName1);或者

Identifier objectName -= functionName1;就会报错。

14.1.5.4、委托运算符 +、-

Identifier objectName = objectName + functionName1 - functionName1;

或者

Identifier objectName = new Identifier(functionName1) + functionName1 - functionName1;

对于这种+、-表达式,在第一个符号+或者-的前面必须是委托而不能是方法,后面的+、-左右都随便。这个不是绝对规律,还有待进一步的研究。

14.1.5.5、多播委托的异常处理

通过一个委托调用多个方法还有一个大问题。多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。下面是MulticastIteration示例。其中定义了一个简单的委托DemoDelegate,它没有参数,返回void。这个委托调用方法One()和Two(),这两个方法满足委托的参数和返回类型要求。注意方法One()抛出了一个异常:

using System;

namespace Wrox.ProCSharp.Delegates
{

    public delegate void DemoDelegate();

    internal class Program
    {
        private static void One()
        {

            Console.WriteLine("One");

            throw new Exception("Error in one");

        }

        private static void Two()
        {

            Console.WriteLine("Two");

        }

        static void Main()
        {

            DemoDelegate d1 = One;

            d1 += Two;

            try
            {

                d1();

            }

            catch (Exception)
            {

                Console.WriteLine("Exception caught");

            }
            Console.ReadKey();
        }

    }

}

在Main()方法中,创建了委托d1,它引用方法One(),接着把Two()方法的地址添加到同一个委托中。调用d1委托,就可以调用这两个方法。异常在try/catch块中捕获。

委托只调用了第一个方法。第一个方法抛出了异常,所以委托的迭代会停止,不再调用Two()方法。当调用方法的顺序没有指定时,结果会有所不同。

One

Exception Caught

注意:

多播委托包含一个逐个调用的委托集合。如果通过委托调用的一个方法抛出了异常,整个迭代就会停止。即如果任一方法引发了异常,而在该方法内未捕获该异常,则该异常将传递给委托的调用方,并且不再对调用列表中后面的方法进行调用。

在这种情况下,为了避免这个问题,应手动迭代方法列表。Delegate类定义了方法GetInvocationList(),它返回一个Delegate对象数组。现在可以使用这个委托调用与委托直接相关的方法,捕获异常,并继续下一次迭代。

using System;

namespace Wrox.ProCSharp.Delegates
{

    public delegate void DemoDelegate();

    internal class Program
    {
        private static void One()
        {

            Console.WriteLine("One");

            throw new Exception("Error in one");

        }

        private static void Two()
        {

            Console.WriteLine("Two");

        }

        static void Main()
        {

            DemoDelegate d1 = One;

            d1 += Two;

            Delegate[] delegates = d1.GetInvocationList();

            foreach (DemoDelegate d in delegates)
            {

                try
                {

                    d();

                }

                catch (Exception)
                {

                    Console.WriteLine("Exception caught");

                }

            }
            Console.ReadKey();
        }
    }

}

修改了代码后运行应用程序,会看到在捕获了异常后,将继续迭代下一个方法。

One

Exception caught

Two

注意:其实如果在多播委托的每个具体的方法中捕获异常,并在内部处理,而不抛出异常,一样能实现多播委托的所有方法执行完毕。这种方式与上面方式的区别在于这种方式的宜昌市在函数内部处理的,上面那种方式的异常是在函数外面捕获并处理的。

14.1.6、通过委托对象来调用它所指向的函数

1、委托实例的名称,后面的括号中应包含调用该委托中的方法时使用的参数。

2、调用委托对象的Invoke()方法,Invoke后面的括号中应包含调用该委托中的方法时使用的参数。

注意:实际上,给委托实例提供括号与调用委托类的Invoke()方法完全相同。因为Invoke()方法是委托的同步调用方法。

注意:不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托(委托对象所指向的函数),调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于事件也是一样的。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一回事了。

14.2、事件

14.2.1、自定义事件

14.2.1.1、声明一个委托:

Delegate result-type delegateName ([parameters]);

这个委托可以在类A内定义也可以在类A外定义。

14.2.1.2、声明一个基于某个委托的事件

Event delegateName eventName;

eventName不是一个类型,而是一个具体的对象,这个具体的对象只能在类A内定义而不能在类A外定义。

14.2.1.3、在类A中定义一个触发该事件的方法

ReturnType FunctionName([parameters])

{

……

If(eventName != null)

{

eventName([parameters]);

或者eventName.Invoke([parameters]);

}

……

}

触发事件之后,事件所指向的函数将会被执行。这种执行是通过事件名称来调用的,就像委托对象名一样的。

触发事件的方法只能在A类中定义,事件的实例化,以及实例化之后的实现体都只能在A类外定义。

14.2.1.4、初始化A类的事件

在类B中定义一个类A的对象,并且让类A对象的那个事件指向类B中定义的方法,这个方法要与事件关联的委托所限定的方法吻合。

14.2.1.5、触发A类的事件

在B类中去调用A类中的触发事件的方法:用A类的对象去调用A类的触发事件的方法。

14.2.1.6、程序实例

using System;

using System.Collections.Generic;
using System.Globalization;
using System.Text;

using System.Threading;

namespace DelegateStudy
{

    public delegate void DelegateClick(int a);

    public class Butt
    {

        public event DelegateClick Click;

        public void OnClick(int a)
        {

            if (Click != null)
                Click.Invoke(a);

            //Click(a);//这种方式也是可以的

            Console.WriteLine("Click()");

        }

    }

    class Program
    {
        public static void Btn_Click(int a)
        {

            for (long i = 0; i < a; i++)

                Console.WriteLine(i.ToString(CultureInfo.InvariantCulture));

        }
        static void Main(string[] args)
        {

            var b = new Butt();

            //在委托中,委托对象如果是null的,直接使用+=符号,会报错,但是在事件中,初始化的时候,只能用+=

            b.Click += Btn_Click; //事件是基于委托的,所以委托推断一样适用,下面的语句一样有效:b.Click += Fm_Click;

            //b.Click(10);错误:事件“DelegateStudy.Butt.Click”只能出现在 += 或 -= 的左边(从类型“DelegateStudy.Butt”中使用时除外)

            b.OnClick(10000);

            Console.ReadLine();

        }
    }
}

14.2.2、控件事件

基于Windows的应用程序也是基于消息的。这说明,应用程序是通过Windows来与用户通信的,Windows又是使用预定义的消息与应用程序通信的。这些消息是包含各种信息的结构,应用程序和Windows使用这些信息决定下一步的操作。

比如:当用户用鼠标去点击一个windows应用程序的按钮的时候,windows操作系统就会捕获到这个点击按钮的动作,这个时候它会根据捕获到的动作发送一个与之对应的预定义的消息给windows应用程序的这个按钮,windows应用程序的按钮消息处理程序会处理接收到的消息,这个程序处理过程就是根据收到的消息去触发相应的事件,事件被按钮触发后,会通知所有的该事件的订阅者来接收这个事件,从而执行相应的的函数。

在MFC等库或VB等开发环境推出之前,开发人员必须处理Windows发送给应用程序的消息。VB和今天的.NET把这些传送来的消息封装在事件中。如果需要响应某个消息,就应处理对应的事件。

14.2.2.1、控件事件委托EventHandler

在控件事件中,有很多的委托,在这里介绍一个最常用的委托EventHandler,.NET Framework中控件的事件很多都基于该委托,EventHandler委托已在.NET Framework中定义了。它位于System命名空间:

Public delegate void EventHandler(object sender,EventArgs e);

14.2.2.2、委托EventHandler参数和返回值

事件最终会指向一个或者多个函数,函数要与事件所基于的委托匹配。事件所指向的函数(事件处理程序)的命名规则:按照约定,事件处理程序应遵循“object_event”的命名约定。object就是引发事件的对象,而event就是被引发的事件。从可读性来看,应遵循这个命名约定。

首先,事件处理程序总是返回void,事件处理程序不能有返回值。其次是参数,只要是基于EventHandler委托的事件,事件处理程序的参数就应是object和EventArgs类型:

第一个参数接收引发事件的对象,比如当点击某个按钮的时候,这个按钮要触发单击事件最终执行这个函数,那么就会把当前按钮传给sender,当有多个按钮的单击事件都指向这个函数的时候,sender的值就取决于当前被单击的那个按钮,所以可以为几个按钮定义一个按钮单击处理程序,接着根据sender参数确定单击了哪个按钮:

if(((Button)sender).Name ==”buttonOne”)

第二个参数e是包含有关事件的其他有用信息的对象。

14.2.2.3、控件事件的其他委托

控件事件还有其他的委托,比如在窗体上有与鼠标事件关联的委托:

Public delegate void MouseEventHandler(object sender,MouseEventArgs e);

public event MouseEventHandler MouseDown;

this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown);

private void Form1_MouseDown(object sender, MouseEventArgs e){};

MouseDown事件使用MouseDownEventArgs,它包含鼠标的指针在窗体上的的X和Y坐标,以及与事件相关的其他信息。

控件事件中,一般第一个参数都是object sender,第二个参数可以是任意类型,不同的委托可以有不同的参数,只要它派生于EventArgs即可。

14.2.2.4、程序实例

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

namespace SecondChangeEvent1
{

    // 该类用来存储关于事件的有效信息外,

    // 还用来存储额外的需要传给订阅者的Clock状态信息

    public class TimeInfoEventArgs : EventArgs
    {

        public TimeInfoEventArgs(int hour, int minute, int second)
        {

            this.hour = hour;

            this.minute = minute;

            this.second = second;

        }

        public readonly int hour;

        public readonly int minute;

        public readonly int second;

    }

    // 定义名为SecondChangeHandler的委托,封装不返回值的方法,

    // 该方法带参数,一个clock类型对象参数,一个TimeInfoEventArgs类型对象

    public delegate void SecondChangeHandler(

    object clock,

    TimeInfoEventArgs timeInformation

    );

    // 被其他类观察的钟(Clock)类,该类发布一个事件:SecondChange。观察该类的类订阅了该事件。

    public class Clock
    {

        // 代表小时,分钟,秒的私有变量

        int _hour;

        public int Hour
        {

            get { return _hour; }

            set { _hour = value; }

        }

        private int _minute;

        public int Minute
        {

            get { return _minute; }

            set { _minute = value; }

        }

        private int _second;

        public int Second
        {

            get { return _second; }

            set { _second = value; }

        }

        // 要发布的事件

        public event SecondChangeHandler SecondChange;

        // 触发事件的方法

        protected void OnSecondChange(

        object clock,

        TimeInfoEventArgs timeInformation

        )
        {

            // Check if there are any Subscribers

            if (SecondChange != null)
            {

                // Call the Event

                SecondChange(clock, timeInformation);

            }

        }

        // 让钟(Clock)跑起来,每隔一秒钟触发一次事件

        public void Run()
        {

            for (; ; )
            {

                // 让线程Sleep一秒钟

                Thread.Sleep(1000);

                // 获取当前时间

                System.DateTime dt = System.DateTime.Now;

                // 如果秒钟变化了通知订阅者

                if (dt.Second != _second)
                {

                    // 创造TimeInfoEventArgs类型对象,传给订阅者

                    TimeInfoEventArgs timeInformation =

                    new TimeInfoEventArgs(

                    dt.Hour, dt.Minute, dt.Second);

                    // 通知订阅者

                    OnSecondChange(this, timeInformation);

                }

                // 更新状态信息

                _second = dt.Second;

                _minute = dt.Minute;

                _hour = dt.Hour;

            }

        }

    }

    /* ======================= Event Subscribers =============================== */

    // 一个订阅者。DisplayClock订阅了clock类的事件。它的工作是显示当前时间。

    public class DisplayClock
    {

        // 传入一个clock对象,订阅其SecondChangeHandler事件

        public void Subscribe(Clock theClock)
        {

            theClock.SecondChange +=

            new SecondChangeHandler(TimeHasChanged);

        }

        // 实现了委托匹配类型的方法

        public void TimeHasChanged(

        object theClock, TimeInfoEventArgs ti)
        {

            Console.WriteLine("Current Time: {0}:{1}:{2}",

            ti.hour.ToString(),

            ti.minute.ToString(),

            ti.second.ToString());

        }

    }

    // 第二个订阅者,他的工作是把当前时间写入一个文件

    public class LogClock
    {

        public void Subscribe(Clock theClock)
        {

            theClock.SecondChange +=

            new SecondChangeHandler(WriteLogEntry);

        }

        // 这个方法本来应该是把信息写入一个文件中

        // 这里我们用把信息输出控制台代替

        public void WriteLogEntry(

        object theClock, TimeInfoEventArgs ti)
        {

            Clock a = (Clock)theClock;

            Console.WriteLine("Logging to file: {0}:{1}:{2}",

            a.Hour.ToString(),

            a.Minute.ToString(),

            a.Second.ToString());

        }

    }

    /* ======================= Test Application =============================== */

    // 测试拥有程序

    public class Test
    {

        public static void Main()
        {

            // 创建clock实例

            Clock theClock = new Clock();

            // 创建一个DisplayClock实例,让其订阅上面创建的clock的事件

            DisplayClock dc = new DisplayClock();

            dc.Subscribe(theClock);

            // 创建一个LogClock实例,让其订阅上面创建的clock的事件

            LogClock lc = new LogClock();

            lc.Subscribe(theClock);

            // 让钟跑起来

            theClock.Run();

        }

    }

}

14. 3、小结

(1)、在定义事件的那个类A里面,可以任意的使用事件名,可以触发;在别的类里面,事件名只能出现在 += 或 -= 的左边来指向函数,即只能实例化,不能直接用事件名触发。但是可以通过A类的对象来调用A类中的触发事件的函数。这是唯一触发事件的方式。

(2)、不管是多播委托还是单播委托,在没有特殊处理的情况下,在一个线程的执行过程中去调用委托(委托对象所指向的函数),调用委托的执行是不会新起线程的,这个执行还是在原线程中的,这个对于事件也是一样的。当然,如果是在委托所指向的函数里面去启动一个新的线程那就是另外一回事了。

(3)、事件是针对某一个具体的对象的,一般在该对象的所属类A中写好事件,并且写好触发事件的方法,那么这个类A就是事件的发布者,然后在别的类B里面定义A的对象,并去初始化该对象的事件,让事件指向B类中的某一个具体的方法,B类就是A类事件的订阅者。当通过A类的对象来触发A类的事件的时候(只能A类的对象来触发A类的事件,别的类的对象不能触发A类的事件,只能订阅A类的事件,即实例化A类的事件),作为订阅者的B类会接收A类触发的事件,从而使得订阅函数被执行。一个发布者可以有多个订阅者,当发布者发送事件的时候,所有的订阅者都将接收到事件,从而执行订阅函数,但是即使是有多个订阅者也是单线程。

SQL with(nolock)详解

大家在写查询时,为了性能,往往会在表后面加一个nolock,或者是with(nolock),其目的就是查询是不锁定表,从而达到提高查询速度的目的。

什么是并发访问:同一时间有多个用户访问同一资源,并发用户中如果有用户对资源做了修改,此时就会对其它用户产生某些不利的影响,例如:

1:脏读

一个用户对一个资源做了修改,此时另外一个用户正好读取了这条被修改的记录,然后,第一个用户放弃修改,数据回到修改之前,这两个不同的结果就是脏读。

2:不可重复读

一个用户的一个操作是一个事务,这个事务分两次读取同一条记录,如果第一次读取后,有另外用户修改了这个数据,然后第二次读取的数据正好是其它用户修改的数据,这样造成两次读取的记录不同,如果事务中锁定这条记录就可以避免。

3:幻读

指用户读取一批记录的情况,用户两次查询同一条件的一批记录,第一次查询后,有其它用户对这批数据做了修改,方法可能是修改,删除,新增,第二次查询时,会发现第一次查询的记录条目有的不在第二次查询结果中,或者是第二次查询的条目不在第一次查询的内容中。

为什么会在查询的表后面加nolock标识?为了避免并发访问产生的不利影响,SQL Server有两种并发访问的控制机制:锁、行版本控制,表后面加nolock是解决并发访问的方案之一。

1.锁

每个事务对所依赖的资源会请求不同类型的锁,它可以阻止其他事务以某种可能会导致事务请求锁出错的方式修改资源。当事务不再依赖锁定的资源时,锁将被释放。

锁的类型:1:表类型:锁定整个表;2:行类型:锁定某个行;3:文件类型:锁定某个数据库文件;4:数据库类型:锁定整个数据库;5:页类型:锁定8K为单位的数据库页。

锁的分类还有一种分法,就是按用户和数据库对象来分:

1). 从数据库系统的角度来看:分为独占锁(即排它锁),共享锁和更新锁

1:共享 (S) :用于不更改或不更新数据的操作(只读操作),一般常见的例如select语句。

2:更新 (U) :用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。

3:排它 (X) :用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。

2). 从程序员的角度看:分为乐观锁和悲观锁。

1:乐观锁:完全依靠数据库来管理锁的工作。

2:悲观锁:程序员自己管理数据或对象上的锁处理。

一般程序员一看到什么锁之类,觉的特别复杂,对专业的DBA当然是入门级知识了。可喜的是程序员不用去设置,控制这些锁,SQLServer通过设 置事务的隔离级别自动管理锁的设置和控制。锁管理器通过查询分析器分析待执行的sql语句,来判断语句将会访问哪些资源,进行什么操作,然后结合设定的隔 离级别自动分配管理需要用到的锁。

2.行版本控制

当启用了基于行版本控制的隔离级别时,数据库引擎 将维护修改的每一行的版本。应用程序可以指定事务使用行版本查看事务或查询开始时存在的数据,而不是使用锁保护所有读取。通过使用行版本控制,读取操作阻 止其他事务的可能性将大大降低。也就是相当于针对所有的表在查询时都会加上nolock,同样会产生脏读的现象,但差别在于在一个统一管理的地方。说到了基于行版本控制的隔离级别,这里有必要说下隔离级别的概念。

隔离级别的用处:控制锁的应用,即什么场景应用什么样的锁机制。

最终目的:解决并发处理带来的种种问题。

隔离级别的分类:

1:未提交读,隔离事务的最低级别,只能保证不读取物理上损坏的数据;

2:已提交读,数据库引擎的默认级;

3:可重复读;

4:可序列化;隔离事务的最高级别,事务之间完全隔离。

小结:NOLOCK 语句执行时不发出共享锁,允许脏读 ,等于 READ UNCOMMITTED事务隔离级别 。nolock确实在查询时能提高速度,但它并不是没有缺点的,起码它会引起脏读。

nolock的使用场景(个人观点):

1:数据量特别大的表,牺牲数据安全性来提升性能是可以考虑的;

2:允许出现脏读现象的业务逻辑,反之一些数据完整性要求比较严格的场景就不合适了,像金融方面等。

3:数据不经常修改的表,这样会省于锁定表的时间来大大加快查询速度。

最后说下nolock和with(nolock)的几个小区别:

1:SQL05中的同义词,只支持with(nolock);

2:with(nolock)的写法非常容易再指定索引。

跨服务器查询语句时 不能用with (nolock) 只能用nolock,同一个服务器查询时 则with (nolock)和nolock都可以用

SQL Server中的Transaction、error check、Lock、Isolation level、save point

Transaction及错误检查

SQL Server 中最重要的知识点莫过于事务,比如很多OLTP(联机事务处理)应用程序。什么是事务?事务就是一系列SQL语句的集合。事务包括隐性事务(例如Insert,Update等语句)和显性事务(用Begin Tran语句显式指明的事务)。事务中通常需要进行错误检查,用@@error来进行检查,比如:

Begin Tran
Update A set id =5 where id=1
If @@error<>0
rollback Tran
Update A set id =5 where id=2
If @@error<>0
rollback Tran
Commit Tran

Lock (锁)

按锁的粒度分,锁可以分成如下几类:

Key Lock(键锁)—>Row Lock(行级锁)—>Page Lock(页级锁)—>Extent Lock(扩展盘曲锁)—>Table Lock(表锁)—>Database Lock(数据库锁)

按锁的模式分,锁可以分为如下几类:

holdlock(共享锁),xlock(排它锁),Updlock(更新锁),Schlock(架构锁),Intent lock(意向锁)等等。如果要查看锁的类型,使用系统存储过程sys_lock来查看。

Isolation level (隔离级别)

事务有4中隔离级别,分别为:
read uncommitted(未提交读) — 读未提交,可以读取到内存中已经修改但是没有保存到硬盘上的信息,即允许数据脏读。
read committed(提交读) — 读提交,只能读取到已经提交到硬盘的信息,如果信息在内存中修改了,但是还没有提交到硬盘,即没有commit tran,则另一个事务什么也读取不到,被另一事物阻塞在此。当修改数据的事务一旦commit tran,则读取数据的事务立即运行,修改后的数据被读取到。
repeatable read(重复读) — 当事务A设置隔离级别为repeatable read在对数据进行读取,此时,事务B来修改数据,由于repeatable read隔离级别对操作的实体(行或者表)设置了更新锁,所以此时事务B不能对数据进行更新,但是事务B可以insert新数据,因为repeatable read隔离级别对操作的实体(行或者表)没有设置排它锁,所以事务A可以读取到幻象。
serializable(串行读) — 串行化,即事务一个接一个地进行操作,包括对操作实体的update,insert,select等等。

实际上隔离级别和锁的关系是密不可分的,隔离级别的实现本质上是对锁来进行操作,由于我们在操作一个实体对象的时候不能准确地判断到底应该上什么具体的锁 ,所以鉴于此,SQL server数据库为我们开辟了一个简单的途径,即使用隔离级别。实体的隔离级别越高,说明实体上锁的数量越多,种类越复杂;实体的隔离级别越高,并行化的几率越低,串行化的几率越高。

Save Point(保存点)

保存点的出现,是为了在事务恢复时更加地迅速和容易,因为不用把所有的操作都恢复,而是只用恢复到保存点即可,关于如何恢复以及更具体的知识,会在事务的恢复博客中详述。举个简单的例子:

Begin Tran
Update A set id =4 where id=1
Save tran t1
Update A set id =3 where id=2
If @@error<>0
rollback t1
Update A set id=5 where id =3
Commit Tran

当ASP.NET发生Viewstate MAC的验证失败(machineKey)

问题是这样的,当 ASP.NET 因为网页还没全部下载完成时,使用者就按下网页中的任意一个PostBack 的按钮或链接时,就会发生「Viewstate MAC 的验证失败」的错误讯息!

这问题实在很难除错(DEBUG),我想很多人连发生的原因都不知道,主要的发生原因有两种:

1. 当网站采用 Web-farm 架构时,也就是一个网站采用负载平衡的架构,用多台 Web 主机同时提供服务时。

因为 ASP.NET 预设会将 Viewstate 编码加密,验证数据的加密类型是 SHA1,验证加密数据的密钥(Key)预设是「自动产生」,所以每一台Web主机所产生的Key都不一样,所以你采用多台主机同时提供服务时,就可能会遇到从第一台Web主机读到的内容,做 PostBack 时可能会 PostBack 到第二台主机,但第二台主机看不懂第一台主机编码过的 Viewstate,而导致「Viewstate MAC 的验证失败」的例外发生!

这时你需要统一每一台主机的 machineKey 才能让每一台的编码加密的内容可以被正确验证!建议您去 The Code Project 网站看这份文件:ASP.NET machineKey Generator 上面有完整说明!

2. 因为网页还没全部下载完成,导致页面的状态不完整时就对服务器发出 PostBack 要求,因为 ViewState 不完整,而导致 Viewstate 验证失败。

这个问题只能将修改网站的 web.config 设定将 Viewstate 全部关闭才不会发生错误!如下:

<pages enableEventValidation="false" viewStateEncryptionMode ="Never" enableViewStateMac="false"/>

对List取交集、联集及差集

前言

最近在项目中,刚好遇到这个需求,需要比对两个List,进行一些交集等操作,在以前我们可能需要写很多行来完成这些动作,但现在我们只需要藉由LinQ就能轻松达到我们的目的啰!

实际演练

※本文使用int为例,若为使用自定义之DataModel,需实现IEquatable接口才能使用

1. 取交集 (A和B都有)

List A : { 1 , 2 , 3 , 5 , 9 }
List B : { 4 , 3 , 9 }

var intersectedList = list1.Intersect(list2);

结果 : { 3 , 9 }

判断A和B是否有交集

bool isIntersected = list1.Intersect(list2).Count() > 0

2. 取差集 (A有,B没有)

List A : { 1 , 2 , 3 , 5 , 9 }
List B : { 4 , 3 , 9 }

var expectedList = list1.Except(list2);

结果 : { 1 , 2 , 5 }

判断A和B是否有差集

bool isExpected = list1.Expect(list2).Count() > 0

3. 取联集 (包含A和B)

List A : { 1 , 2 , 3 , 5 , 9 }
List B : { 4 , 3 , 9 }

var result = A.union(B)

结果 : { 1 , 2 , 3 , 5 ,9 , 4 }

RBAC基于角色的访问控制

基于角色的访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。

RBAC支持三个著名的安全原则:最小权限原则,责任分离原则和数据抽象原则。最小权限原则之所以被RBAC所支持,是因为RBAC可以将其角色配置成其完成任务所需要的最小的权限集。责任分离原则可以通过调用相互独立互斥的角色来共同完成敏感的任务而体现,比如要求一个计帐员和财务管理员共参与同一过帐。数据抽象可以通过权限的抽象来体现,如财务操作用借款、存款等抽象权限,而不用操作系统提供的典型的读、写、执行权限。然而这些原则必须通过RBAC各部件的详细配置才能得以体现。

RBAC有许多部件,这使得RBAC的管理多面化。尤其是,我们要分割这些问题来讨论:用户与角色的指派;角色与权限的指派;为定义角色的继承进行的角色与角色的指派。这些活动都要求把用户和权限联系起来。然而在很多情况下它们最好由不同的管理员或管理角色来做。对角色指派权限是典型的应用管理者的职责。银行应用中,把借款、存款操作权限指派给出纳角色,把批准贷款操作权限指派给经理角色。而将具体人员指派给相应的出纳角色和管理者角色是人事管理的范畴。角色与角色的指派包含用户与角色的指派、角色与权限的指派的一些特点。更一般来说,角色与角色的关系体现了更广泛的策略。

RBAC认为权限授权实际上是Who、What、How的问题。在RBAC模型中,who、what、how构成了访问权限三元组,也就是“Who对What(Which)进行How的操作”。 Continue reading “RBAC基于角色的访问控制”

MVVM模式

MVVM概述

MVVM是Model-View-ViewModel的简写。
微软的WPF带来了新的技术体验,如Sliverlight、音频、视频、3D、动画……,这导致了软件UI层更加细节化、可定制化。同时,在技术层面,WPF也带来了 诸如Binding、Dependency Property、Routed Events、Command、DataTemplate、ControlTemplate等新特性。MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构框架。它立足于原有MVP框架并且把WPF的新特性揉合进去,以应对客户日益复杂的需求变化。

MVVM 功能图

mvvm

实例解析

WPF的数据绑定与Presentation Model相结合是非常好的做法,使得开发人员可以将View和逻辑分离出来,但这种数据绑定技术非常简单实用,也是WPF所特有的,所以我们又称之为Model-View-ViewModel(MVVM)。这种模式跟经典的MVP(Model-View-Presenter)模式很相似,除了你需要一个为View量身定制的model,这个model就是ViewModel。ViewModel包含所有由UI特定的接口和属性,并由一个 ViewModel 的视图的绑定属性,并可获得二者之间的松散耦合,所以需要在ViewModel 直接更新视图中编写相应代码。数据绑定系统还支持提供了标准化的方式传输到视图的验证错误的输入的验证。
在视图(View)部分,通常也就是一个Aspx页面。在以前设计模式中由于没有清晰的职责划分,UI 层经常成为逻辑层的全能代理,而后者实际上属于应用程序的其他层。MVP 里的M 其实和MVC里的M是一个,都是封装了核心数据、逻辑和功能的计算关系的模型,而V是视图(窗体),P就是封装了窗体中的所有操作、响应用户的输入输出、事件等,与MVC里的C差不多,区别是MVC是系统级架构的,而MVP是用在某个特定页面上的,也就是说MVP的灵活性要远远大于MVC,实现起来也极为简单。
我们再从IView这个interface层来解析,它可以帮助我们把各类UI与逻辑层解耦,同时可以从UI层进入自动化测试(Unit/Automatic Test)并提供了入口,在以前可以由WinForm/Web Form/MFC等编写的UI是通过事件Windows消息与IView层沟通的。WPF与IView层的沟通,最佳的手段是使用Binding,当然,也可以使用事件;Presenter层要实现IView,多态机制可以保证运行时UI层显示恰当的数据。比如Binding,在程序中,你可能看到Binding的Source是某个interface类型的变量,实际上,这个interface变量引用着的对象才是真正的数据源。
MVC模式大家都已经非常熟悉了,在这里我就不赘述,这些模式也是依次进化而形成MVC—>MVP—>MVVM。有一句话说的好:当物体受到接力的时候,凡是有界面的地方就是最容易被撕下来的地方。因此,IView作为公共视图接口约束(契约)的一层意思;View则能传达解耦的一层意思。

设计模式

因为WPF技术出现,从而使MVP设计模式有所改进,MVVM 模式便是使用的是数据绑定基础架构。它们可以轻松构建UI的必要元素。
可以参考The Composite Application Guidance for WPF(prism)
View绑定到ViewModel,然后执行一些命令在向它请求一个动作。而反过来,ViewModel跟Model通讯,告诉它更新来响应UI。这样便使得为应用构建UI非常的容易。往一个应用程序上贴一个界面越容易,外观设计师就越容易使用Blend来创建一个漂亮的界面。同时,当UI和功能越来越松耦合的时候,功能的可测试性就越来越强。
在MVP模式中,为了让UI层能够从逻辑层上分离下来,设计师们在UI层与逻辑层之间加了一层interface。无论是UI开发人员还是数据开发人员,都要尊重这个契约、按照它进行设计和开发。这样,理想状态下无论是Web UI还是Window UI就都可以使用同一套数据逻辑了。借鉴MVP的IView层,养成习惯。View Model听起来比Presenter要贴切得多;会把一些跟事件、命令相关的东西放在MVC的’C’,或者是MVVM的’Vm’

MVVM优点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大有点:

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
  2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
  3.  独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。
  4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

MVVM控件

使用MVVM来开发用户控件[1]。由于用户控件在大部分情况下不涉及到数据的持久化,所以如果将M纯粹理解为DomainModel的话,使用MVVM模式来进行自定义控件开发实际上可以省略掉M,变成了VVM。

浅谈javascript继承的设计思想

我一直很难理解Javascript语言的继承机制。

它没有”子类”和”父类”的概念,也没有”类”(class)和”实例”(instance)的区分,全靠一种很奇特的”原型链”(prototype chain)模式,来实现继承。

我花了很多时间,学习这个部分,还做了很多笔记。但是都属于强行记忆,无法从根本上理解。

直到昨天,我读到法国程序员Vjeux的解释,才恍然大悟,完全明白了Javascript为什么这样设计。

下面,我尝试用自己的语言,来解释它的设计思想。彻底说明白prototype对象到底是怎么回事。其实根本就没那么复杂,真相非常简单。

Continue reading “浅谈javascript继承的设计思想”

新手PM会遇到的五个疑问

文章主要针对新手PM在参加PM培训之后遇到的一些疑问,包括项目失控、PM如何建立威信、团队问题、跨团队合作、PM要不要对技术方案负责等疑问进行一一解答,希望对新手PM和准备踏进PM界的朋友有所帮助。

只有不断的学习,才能不断的进步!

一. 项目失控怎么办?

产生这种情况说明项目管理已经存在大的问题了。要做到的是提前预知,避免这种情况的出现。

万一出现了,首先要深入了解原因。多问自己问题,是人的原因吗?因为开发是新人?负面情绪引发懈怠?还是沟通不畅? 再看如何解决。

1.项目内搞定:

项目内可以搞定吗?回答这个问题的关键是找关键路径。看从非关键路径是否可以抽调资源到关键路径上。这是要注意关键路径的变化。如果没法解决,项目外解决。

2.项目外搞定:

项目外也就是看项目管理三角形,在资源、时间、范围上找解决方法。

二. PM如何建立威信?

其实PM需要建立的不是威信,而是信任。这里提到一个非常有趣的概念,Johari Windows。乔哈里之窗能够用来展现、提高个人与组织的自我意识,也可以用来改变整个组织的动态信息沟通系统。

乔哈里之窗把人的内心世界比作一个四格的窗口:

The Open Arena:开放区,自己和他人都知道的领域。

The Hidden Facade:隐藏区,自己知道别人不知道的领域。

The Blind Spot:盲区,别人知道但自己不知道的领域。

The Closed Area:封闭区,双方都不了解的领域。

真正有效的沟通,只能在公开区内进行。因为在此区域内,双方交流的主题是共知的,沟通效果是会令双方满意的。实际沟通中,很多情况下信息不对等,处于封闭区,导致沟通无效。要使得沟通更有效,需要增强信息的真实度、透明度,进而扩大开放区。扩大开放区的方式可以经由自我坦诚,或经由反馈。

三. 遇到能力强但固执的项目成员怎么办?

1. 识别出其固执的原因。

2. 和其身边的朋友多沟通,对其个性和行事风格等更多了解。

3. 使其承担责任,成为某个课题的owner,自我价值实现。

4. 多搞团建,和团队融合。

5. 不能被团队认可,请出项目组。

四. 跨团队的资源如何协调?

1. 要清楚的认识到,我们不是去要资源,而是要取得一致目标。是我们大家一起要把这个事情做出来。讲讲清楚要做什么,得到认可,把事情变成大家的事情,而不是变成对立。

2. 要有结果,要及时反馈。

五. PM要对技术方案负责吗?

PM对技术方案影响越少越好,要认清PM的职责,是带领团队在竞争中取胜。要识别出可以对技术方案或业务拍板的关键人物,他们去负责。

这里又涉及到PM正确的心态应该是怎样的:

一、善假于力,融会贯通。对人,要知人善任,了解团队,了解成员的长处短处。对事,项目管理各个环节都要融汇进去。

二、正向关注,助人自助。对人,给人机会,也要慎重淘汰。对事,接受挑战,积极正向。

.Net下的开源持续集成

谈到持续集成,不如先谈谈集成。软件开发中的集成,通俗地讲就是把各个相关部分的东西组合起来,形成一个可用的软件。比如一个软件项目由几个小组来负责完成,每个小组负责其中一部分功能的实现,比较典型的是在现在的网络游戏开发中,通常有负责引擎的小组,负责游戏逻辑的小组,负责美工的小组,这些小组开发出的东西必须结合在一起才能形成一个可用的游戏;而每个小组内部的每个成员,他们每天在写着不同部分的代码,这些开发人员必须将各自的成果组合起来,才能完成他们共同的目标。从这个意义上讲,从每天每个开发者的日常开发,到各个软件模块的组合拼接,软件集成无所不在,一个完整可用的软件,就是通过不断地集成每个开发人员的代码而形成的。

每个开发过软件的人都能体会到,软件开发绝非一帆风顺,每个人开发出来的代码绝对不会魔术般的自己组合在一起,当新的功能加入到原有软件中的时候,往往不小心破坏了原有的功能,引入了一些bug,当老的bug被修复的时候,又往往会导致其他bug的产生,更糟糕的是,这些bug往往在当时并不能及时被发现。当小组成员们完成了自己所负责的模块,等到最后来一起集成的时候,他们可能已经做好了修复集成问题的心理准备。所有的这一切,都是每个开发人员的切肤之痛。那么,问题到底出在哪了?

让我们不妨换一种思维,让我们不要停留在如何地去修复bug和集成过程中产生的问题,我们渴望的理想状况是,每当我们开发出新的代码并将他们加入原系统中的时候,如果我们能够被及时告知我们是否破坏了原有系统的功能,那么我就能够及时的作出反应,修复这些部分。如果这个过程的粒度足够的细、足够地频繁,我们就能期望每次新功能引入的时候所引起的破坏足够小,并且修复起来足够简单,如果每个开发人员都能够享受到如此的方便并且保证新加入的功能不会影响原有的功能,我们的整个软件过程就能够以一个稳步可靠的步伐,持续增量的向前行进,而不是不断地加入新的代码,然后等到后来bug被人发现的时候被动地去修复它。我想稳步可靠、持续增量的软件过程,是我们每个开发者心目中的理想过程。从开发者的角度来看,毕竟谁也不想经历那种当发现自己的修改破坏的原有的功能的时候的恼火的感受。

持续集成通俗地说就是持续地、频繁地进行集成,每当有新的修改加入的时候,修改的作者能够被及时地告知他的修改是否在引入新的功能的同时保证原有功能的完整。如果整个软件开发团队在一开始就采用这种方式,我们的软件就能被稳步可靠的构建起来。

你也许会有疑问:“你说的只是一种理想状况罢了,谁都希望自己在加入新的代码的时候得知自己的代码是否破坏了原有的功能,但是怎么能够做到这一点,谁有能力来及时地告诉我们哪里有问题?持续集成这个想法不错,但怎么样能够做到持续集成?”我想这些问题是非常好也是关键的问题,持续集成究竟是否可行,如果可行,又该如何执行?我们这里不妨来整理一下,看看究竟什么是持续集成的难点。持续集成的难点主要在于,在新的功能加入的时候,如何来判断整个系统功能仍然完整;出错或者成功,谁来告诉我,如何告诉我;当大家一起协作的时候,如何保证每个人都能够准确地被告知而不会发生混乱。让我们来一个个地分析这些问题。

首先,确保真个系统功能完整性的手段就是测试,如果我们对所有的功能都有完整的测试,那么当新的功能引入的时候,如果某些原有的测试失败,就说明新的修改破坏了原有的功能,而失败的测试就能准确地告诉我们新的修改破坏了哪些原有的功能。其次,持续集成工具将告诉我们集成是否成功,持续集成工具通过运行整个系统中的测试,根据测试的结果来通知开发者,哪些测试失败导致的集成失败。每个软件项目通常会使用版本控制工具例如SVN、CVS,每当有开发者将新的修改加入到系统的代码库中时,持续集成工具会check out出代码库中的最新版本,使用自动化的构建工具例如Ant、Rake等,自动地编译项目中的代码、部署整个应用、准备测试所需的环境和数据、运行所有的测试包括单元测试、功能测试、集成测试等,在整个过程结束后将结果报告出来,持续集成工具会指出任何一个过程中出现的错误,并且准确地报告给开发者。在多人协作的情况下,版本控制工具确保了每个开发者的修改被正确有序地保存,当每个开发者想要提交自己的修改的之前,必须首先确保上一个人所提交的修改被成功集成,才能提交自己的代码,当确保自己的代码被正确集成之后,自己的工作才算完成,否则,就必须修复错误,再次提交,如此反复,直到被成功集成。

开源社区已经为我们提供了非常优秀的持续集成工具,CruiseControl、CruiseControl .Net已成为广泛使用而且非常成熟的持续集成工具,而持续集成所需要的自动化构建工具和版本管理工具如Ant、NAnt、SVN也已经是非常成熟。在下面,我尝试在我的使用经验的感受的基础上,挑选一些比较成熟或者很有潜力的工具,结合自己的使用经验,给大家做一些介绍。 Continue reading “.Net下的开源持续集成”