C#知识点提要

本篇博文主要对asp.net mvc开发需要撑握的C#语言知识点进行简单回顾,尤其是C# 3.0才有的一些C#语言特性。对于正在学asp.net mvc的童鞋,不防花个几分钟浏览一下。本文要回顾的C#知识点有:特性、自动属性、对象集合初始化器、扩展方法、Lambda表达式和Linq查询。C#资深“玩家”可路过。

1.特性(Attributes)

特性(Attributes),MSDN的定义是: 公共语言运行时允许你添加类似关键字的描述声明,叫做attributes, 它对程序中的元素进行标注,如类型、字段、方法和属性等。Attributes和Microsoft .NET Framework文件的元数据保存在一起,可以用来向运行时描述你的代码,或者在程序运行的时候影响应用程序的行为。 例如,在一个方法前标注[Obsolete]特性,则调用该方法时VS则会提示该方法已过期的警告,如下图:

又如,在.Net Remoting 的远程对象中,如果要调用或传递某个对象,例如类,或者结构,则该类或结构则必须标注[ Serializable ]特性。还有,我们在构建XML Web服务时用得很多的一个特性就是[WebMegthod],它可让通过HTTP请求的公开方法的返回值编码成XML进行传递。

特性实际上就是一个类,[Obsolete]特性的实际类名是ObsoleteAttribute,但我们在标注的时候可以不带Attribute后缀,系统在名称转换时会自动给我们加上。

上面说的都是些.NET系统定义的一些特性,当然还有很多。了解如何自定义特性,有利有我们更好的在ASP.NET MVC编程使用特性,比如给Model类的属性标注特性来验证表单输入的合法性(以后进行介绍)。

下面我们来模拟一个ASP.NET MVC经常要用到的StringLenth特性,它用于判断用户输入是否超出长度限制。我们现在来模拟它。先定义一个MyStringLenth特性:

// 用户自定义的带有可选命名参数的 MyStringLenthAttribute 特性类。
// 该特性通过AttributeUsage限制它只能用在属性和字段上。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public sealed class MyStringLenthAttribute : Attribute {
    public MyStringLenthAttribute(string displayName, int maxLength) {
        this.MaxLength = maxLength;
        this.DisplayName = displayName;
    }
    //显示的名称,对外是只读的,所以不能通过可选参数来赋值,必须在构造函数中对其初始化。
    public string DisplayName { get; private set; }

    //长度最大值,对外是只读的,所以不能通过可选参数来赋值,必须在构造函数中对其初始化。
    public int MaxLength { get; private set; }

    //错误信息,标注时可作为可选命名参数来使用。
    public string ErrorMessage { get; set; }

    //长度最小值,标注时可作为可选命名参数来使用。
    public int MinLength { get; set; }
}

上面若不加AttributeUsage限制,特性可以声明在类型(如结构、类、枚举、委托)和成员(如方法,字段,事件,属性,索引)的前面。

然后我们把这个特性应用在下面的Order类之上:

// 应用自定义MyStringLenth特性于Order类的OrderID属性之上。MinLength和ErrorMessage是命名参数。
public class Order {
    [MyStringLenth("订单号", 6,MinLength = 3, ErrorMessage = "{0}的长度必须在{1}和{2}之间,请重新输入!")]
    public string OrderID { get; set; }
}

最后我们看看如何使用MyStringLenth特性验证用户输入字符串的长度:

//检查成员字符串长度是否越限。
private static bool IsMemberValid(int inputLength, MemberInfo member) {
    foreach (object attribute in member.GetCustomAttributes(true)) {
        if (attribute is MyStringLenthAttribute) {
            MyStringLenthAttribute attr=(MyStringLenthAttribute)attribute;
            string displayName = attr.DisplayName;
            int maxLength = attr.MaxLength;
            int minLength = attr.MinLength;
            string msg = attr.ErrorMessage;

            if (inputLength < minLength || inputLength > maxLength) {
                Console.WriteLine(msg, displayName, minLength, maxLength);
                return false;
            }
            else {
                return true;
            }
        }
    }
    return false;
}

//验证输入是否合法
private static bool IsValid(Order order) {
    if (order == null) return false;

    foreach (PropertyInfo p in typeof(Order).GetProperties()) {
        if (IsMemberValid(order.OrderID.Length, p))
            return true;
    }

    return false;
}

public static void Main() {
    string input=string.Empty;
    Order order;
    do {
        Console.WriteLine("请输入订单号:");
        input = Console.ReadLine();
        order = new Order { OrderID = input };
    }
    while (!IsValid(order));
    Console.WriteLine("订单号输入正确,按任意键退出!");
    Console.ReadKey();
}

输出效果如下:

2.自动属性

在 C# 3.0 和更高版本中,当属性的访问器中不需要其他逻辑时,自动实现的属性可使属性声明更加简洁。

下面示例演示了属性的标准实现和自动实现:

class Program {
    class Person {
        //标准实现的属性
        int _age;
        public int Age {
            get { return _age; }
            set {
                if (value < 0 || value > 130) {
                    Console.WriteLine("设置的年龄有误!");
                    return;
                }
                _age = value;
            }
        }

        //自动实现的属性
        public string Name { get; set; }
    }
        
    static void Main(string[] args) {
        Person p = new Person();
        p.Age = 180;
        p.Name = "小王";
        Console.WriteLine("{0}今年{1}岁。",p.Name,p.Age);
        Console.ReadKey();
    }
}

自动属性也可以有不同的访问权限,如:

public string Name { get;private set; }

注意,自动属性 不能定义只读或者只写的属性,必须同时提供get和set访问器:

public string Name { get; }//编译出错
public string PetName { set; }//编译出错

3.对象和集合的初始化器

上面我们演示自动属性的时候给对象的实例初始化时是一个一个属性进行赋值的,有多少个属性就需要多少句代码。C# 3.0和 更高版本中 有了对象集合初始化器,有了它,只需一句代码就可初始化一个对象或一个对象集合的所有属性。这在里先创建一个“商品”类,用于后面的示例演示:

/// <summary>
/// 商品类
/// </summary>
public class Product {
    /// <summary>
    /// 商品编号
    /// </summary>
    public int ProductID { get; set; }
    /// <summary>
    /// 商品名称
    /// </summary>
    public string Name { get; set; }
    /// <summary>
    /// 商品描述
    /// </summary>
    public string Description { get; set; }
    /// <summary>
    /// 商品价格
    /// </summary>
    public decimal Price { get; set; }
    /// <summary>
    /// 商品分类
    /// </summary>
    public string Category { set; get; }
}

基于上面定义好的商品类,下面代码演示了如何通过初始化器来创建商品类的实例对象和集合:

static void Main(string[] args) {
    //对象初始化器的使用 (可只给部分字段赋值)
    Product product = new Product { ProductID = 1234, Name = "西瓜", Price = 2.3M };//创建并初始化一个实例

    //集合初始化器的使用
    List<Product> proList = new List<Product> { 
        new Product { ProductID = 1234, Name = "西瓜", Price = 2.3M },
        new Product { ProductID = 2345, Name = "苹果", Price = 5.9M  },
        new Product { ProductID = 3456, Name = "樱桃", Price = 4.6M }
    };

    //打印
    Console.WriteLine("对象初始化器:{0} {1} {2}", product.ProductID, product.Name, product.Price);
    foreach (Product p in proList) {
        Console.WriteLine("集合初始化器:{0} {1} {2}", p.ProductID, p.Name, p.Price);
    }
    Console.ReadKey();
}

另外还有一些其它类型也可以使用初始化器,如下:

//数组使用初始化器
string[] fruitArray = {"apple","orange","plum" };
//匿名类型使用初始化器
var books = new { Title = "ASP.NET MVC 入门", Author = "小王", Price = 20 };
//字典类型使用初始化器
Dictionary<string, int> fruitDic = new Dictionary<string, int>() { 
    { "apple", 10 },
    { "orange", 20 },
    { "plum", 30 }
};

4.扩展方法

扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型或修改原始类型。 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。例如,我们可以让Random类的所有实例对象拥有一个返回随机bool值的方法。我们不能对Random类本身进行修改,但可以对它进行扩展,如下代码所示:

static class Program {
    /// <summary>
    /// 随机返回 true 或 false
    /// </summary>
    /// <param name="random">this参数自动指定到Random的实例</param>
    /// <returns></returns>
    public static bool NextBool(this Random random) {
        return random.NextDouble() > 0.5;
    }

    static void Main(string[] args) {
        //调用扩展方法
        Random rd = new Random();
        bool bl = rd.NextBool();

        Console.WriteLine(bl.ToString());
        Console.ReadKey();
    }
}

注意,扩展方法必须在非泛型的静态类中定义,上面的Program类如不加static修饰符则会报错。

我们可以创建一个接口的扩展方法,这样实现该接口的类都可以调用该扩展方法。看下面一个完整示例:

/// <summary>
/// 购物车类 (实现 IEnumerable<Product> 接口)
/// </summary>
public class ShoppingCart : IEnumerable<Product> {
    public List<Product> Products { get; set; }
    public IEnumerator<Product> GetEnumerator() {
        return Products.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }
}

/// <summary>
/// 定义一个静态类,用于实现扩展方法(注意:扩展方法必须定义在静态类中)
/// </summary>
public static class MyExtensionMethods {
    /// <summary>
    /// 计算商品总价钱
    /// </summary>
    public static decimal TotalPrices(this IEnumerable<Product> productEnum) {
        decimal total = 0;
        foreach (Product prod in productEnum) {
            total += prod.Price;
        }
        return total;
    }
}

class Program {
    static void Main(string[] args) {
        // 创建并初始化ShoppingCart实例,注入IEnumerable<Product>
        IEnumerable<Product> products = new ShoppingCart {
            Products = new List<Product> { 
                new Product {Name = "Kayak", Price = 275}, 
                new Product {Name = "Lifejacket", Price = 48.95M}, 
                new Product {Name = "Soccer ball", Price = 19.50M}, 
                new Product {Name = "Corner flag", Price = 34.95M}
            }
        };
        // 创建并初始化一个普通的Product数组
        Product[] productArray = { 
            new Product {Name = "Kayak", Price = 275M}, 
            new Product {Name = "Lifejacket", Price = 48.95M}, 
            new Product {Name = "Soccer ball", Price = 19.50M}, 
            new Product {Name = "Corner flag", Price = 34.95M} 
        };

        // 取得商品总价钱:用接口的方式调用TotalPrices扩展方法。
        decimal cartTotal = products.TotalPrices();
        // 取得商品总价钱:用普通数组的方式调用TotalPrices扩展方法。
        decimal arrayTotal = productArray.TotalPrices();

        Console.WriteLine("Cart Total: {0:c}", cartTotal);
        Console.WriteLine("Array Total: {0:c}", arrayTotal);
        Console.ReadKey();
    }
}

执行后输出如下结果:

5.Lambda 表达式

Lambda 表达式和匿名函数其实是一件事情。不同是,他们语法表现形式不同,Lambda 表达式在语法上实际上就是匿名函数的简写。直接介绍匿名函数和Lambda表达式的用法没什么意思,在这里,我要根据实际应用来讲一个两者用法的例子,这样在介绍知识点的同时也能和大家分享一下解决问题的思想。

假如我们要实现一个功能强大的商品查询方法,这个商品查询方法如何查询商品是可以由用户自己来决定的,用户可以根据价格来查询商品,也可以根据分类来查询商品等等,也就是说用户可以把自己的查询逻辑传递给这个查询方法。要编写这样一个方法,我们很自然的会想到用一个委托来作为这个方法的参数,这个委托就是用户处理商品查询的逻辑。 我们不防把这个查询方法称为“商品查询器”。我们可以用静态的扩展方法来实现这个“商品查询器“,这样每个商品集合对象(如 IEnumerable<Product> products)可以直接调用该静态方法返回查询结果。解决问题的思想有了,接下来就是实现了。或许你对这一段描述有点蒙,结合代码可能让你更清晰。下面是这个“商品查询器”-Filter方法的实现代码:

/// <summary>
/// 定义一个静态类,用于实现扩展方法
/// </summary>
public static class MyExtensionMethods {
    /// <summary>
    /// 商品查询器
    /// </summary>
    /// <param name="productEnum">扩展类型的实例引用</param>
    /// <param name="selectorParam">一个参数类型为Product,返回值为bool的委托</param>
    /// <returns>查询结果</returns>
    public static IEnumerable<Product> Filter(this IEnumerable<Product> productEnum, Func<Product, bool> selectorParam) {
        foreach (Product prod in productEnum) {
            if (selectorParam(prod)) {
                yield return prod;
            }
        }
    }
}

没错,我们就是用这么简短的Filter方法来满足各种需求的查询。上面Product类使用的是前文定义的。这里也再一次见证了扩展方法的功效。为了演示Filter查询方法的调用,我们先来造一批数据:

static void Main(string[] args) {
    // 创建商品集合
    IEnumerable<Product> products = new ShoppingCart {
        Products = new List<Product> { 
            new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
            new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
            new Product {Name = "ASP.NET MCV 入门", Category = "书籍", Price = 19.5M}, 
            new Product {Name = "ASP.NET MCV 提高", Category = "书籍", Price = 34.9M} 
        }
    };
}

接下来我们继续在上面Main方法中来调用查询方法Filter:

//用匿名函数定义一个具体的查询需求
Func<Product, bool> fruitFilter = delegate(Product prod) {
    return prod.Category == "水果";
};

//调用Filter,查询分类为“水果”的商品
IEnumerable<Product> filteredProducts = products.Filter(fruitFilter);

//打印结果
foreach (Product prod in filteredProducts) {
    Console.WriteLine("商品名称: {0}, 单价: {1:c}", prod.Name, prod.Price);
} 
Console.ReadKey();

输出结果为:

上面我们使用的是委托和匿名函数来处理用户查询逻辑,并把它传递给Filter方法,满足了前面所说的需求。但若使用Lambda表达式代替上面的匿名函数能使上面的代码看上去更简洁更人性化,如下代码所示:

Func<Product, bool> fruitFilter = prod => prod.Category == "水果";
IEnumerable<Product> filteredProducts = products.Filter(fruitFilter);

没有了delegate关键字,没有了大小括号,看上去更舒服。当然上面两行代码可以继续简化为一行:

IEnumerable<Product> filteredProducts = products.Filter(prod => prod.Category == "水果");

这三种方式输出结果都是一样的。然后,我们还可以通过Lambda表达式实现各种需求的查询:

//查询分类为“水果”或者单价大于30元的商品
IEnumerable<Product> filteredProducts = products.Filter(prod =>
    prod.Category == "水果" || prod.Price > 30
);

通过这个示例,相信大家已经清晰的了解并撑握了Lambda表达式的简单应用,而这就足够了:)。

6.LINQ

最后简单回顾一下LINQ。LINQ(Language Integrated Query语言集成查询)是 VS 2008 和 .NET Framework 3.5 版中一项突破性的创新,它在对象领域和数据领域之间架起了一座桥梁。

上面讲Lambda表达式时,用到的查询结果集的方式未免还是有点麻烦(因为自定义了一个Filter扩展方法),而Linq本身就集合了很多扩展方法,我们可以直接使用,大大的简化了编写查询代码的工作。例如,对于这样一个数据集合:

Product[] products = {
    new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
    new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
    new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 
    new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 
};

如果要查询得到价钱最高的三个商品信息,如果不使用Linq,我们可能会先写一个排序方法,对products根据价钱由高到低进行排序,排序时需要创建一个新的Product[]对象用于存储排序好的数据。但用Linq可大大减少工作量,一两句代码就能搞定。如下代码所示,查出价钱最高的三个商品:

var results = from product in products
                orderby product.Price descending
                select new {
                    product.Name,
                    product.Price
                };
//打印价钱最高的三个商品
int count = 0;
foreach (var p in results) {
    Console.WriteLine("商品:{0},价钱:{1}", p.Name, p.Price);
    if (++count == 3) break;
}
Console.ReadKey();

输出结果:

能熟练使用Linq是一件很爽的事情。上面的Linq语句和我们熟悉的SQL查询语句类似,看上去非常整洁且易懂。但并不是每一种SQL查询语句在C#都有对应的关键字,有时候我们需要使用另外一种Linq查询方式,即“点号”方式的Linq查询方式,这种方式中的Linq查询方法都是扩展方法。如下面这段代码和上面实现的效果是一样的:

var results = products
    .OrderByDescending(e => e.Price)
    .Take(3)
    .Select(e => new { e.Name,e.Price});

foreach (var p in results) {
    Console.WriteLine("商品:{0},价钱:{1}", p.Name, p.Price);
}
Console.ReadKey();

虽然类SQL的Linq查询方式比这种方式看上去更一目了然,但并不是每一种SQL查询语句在C#都有对应的关键字,比如这里的Take扩展方法就是类SQL的Linq查询语法没有的功能。

注意,有些Linq扩展方法分为“延后查询”(deferred)和“即时查询”(immediate)。延后查询意思是拥有“延后查询”扩展方法的Linq语句只有当调用结果集对象的时候才开始真正执行查询,即时查询则是立即得到结果。比如上面的Linq语句的OrderByDescending扩展方法就是一个“延后查询”方法,当程序执行到Linq语句定义部分时并没有查询出结果并放到results对象中,而是当程序执行到foreach循环时才真正执行Linq查询语句得到查询结果。我们可以做个测试,在Ling语句之后,我们再将products[1]对象重新赋值,如下代码所示:

var results = products
    .OrderByDescending(e => e.Price)
    .Take(3)
    .Select(e => new { e.Name, e.Price });

//在Linq语句之后对products[1]重新赋值
products[1] = new Product { Name = "榴莲", Category = "水果", Price = 22.6M };

//打印
foreach (var p in results) {
    Console.WriteLine("商品:{0},价钱:{1}", p.Name, p.Price);
}
Console.ReadKey();

输出结果为:

我们发现results是重新赋值之后的结果。可想而知,查询语句是在results被调用之后才真正执行的。

Linq非常强大也非常好用,这里只是把它当作一个学习ASP.NET MVC之前需掌握的知识点进行简单回顾。要灵活熟练地使用Linq还需要经常使用才行。

参考: 《Pro ASP.NET MVC 3 Framework》