2015年10月

《CLR via C#》---字符、字符串和文本处理

字符

.NET Framework中,每个字符都是System.Char结构的一个实例。System.Char类型很简单,提供了两个公共只读常量字段:MinValueMaxValue

GetNumericValue方法返回字符的数值形式。

     public static void Go() {
      Double d;

      // 数字3
      d = Char.GetNumericValue('\u0033'); // ‘3’ would work too
      Console.WriteLine(d.ToString());       // Displays "3"

      // 普通分数 四分之一
      d = Char.GetNumericValue('\u00bc');
      Console.WriteLine(d.ToString());       // Displays "0.25"

      // 'A'是大写拉丁字母A
      d = Char.GetNumericValue('A');
      Console.WriteLine(d.ToString());      // Displays "-1"
    }

可以使用三种技术来实现各种数值类型和Char实例的相互转换。下面按照优先顺序来列出

  • 转型(强制类型转换),最常用

  • 使用Convert类型

  • 使用IConvertible接口

system.String类型

String类型直接拍绳子Object,所以是引用类型。因此String类型总是存在在堆上,永远不会跑到线程栈。
提示:如果想换行或者回车,应该使用Environment.NewLine方式。
可以使用C#的+操作符直接将几个字符串连成一个。
最后,C#提供了一种特殊的字符串处理方式,称为逐字字符串,通常用于指定文件或目录的路径,或者与正则表达式配合使用。

String file= "c:\\Windows\\System32\\Notepad.exe";
//或者
String file = @"c:\Windows\System32\NotePad.exe";

字符串是不可变的

String对象最重要的一点就是不可变。也就是说,字符串一经创建就不可以更改。意味着,它允许在一个字符串上执行各种操作,而不实际的更改这个字符串。如果需要频繁的更改字符串,请使用StringBuilder类。

比较字符串

强烈建议使用EqualsCompare等方法,不适用CompareTo==!=等操作符。

字符串留用

C#默认不启用字符串留用

高效构造字符串

使用StringBuilder类。

StringBuilder sb = new StringBuilder();

有多个构造器,下面解释一些概念

  • 最大容量

    一个Int32值,指定了能放到字符串中的最大字符数

  • 容量

    指定了有StringBuilder维护的字符串的长度。如果事先知道要在StringBuilder中放入多少个字符,那么构造StringBilder时,应该自己设置容量。

  • 字符数组

    一个有char结构构成的数组,负责维护字符串的字符内容。

    public static void Go() {
      // Construct a StringBuilder to do string manipulations.
      StringBuilder sb = new StringBuilder();

      // Perform some string manipulations using the StringBuilder.
      sb.AppendFormat("{0} {1}", "Jeffrey", "Richter").Replace(" ", "-");

      // Convert the StringBuilder to a String in 
      // order to uppercase all the characters.
      String s = sb.ToString().ToUpper();

      // Clear the StringBuilder (allocates a new Char array).
      sb.Length = 0;

      // Load the uppercase String into the StringBuilder, 
      // and do more manipulations.
      sb.Append(s).Insert(8, "Marc-");

      // Convert the StringBuilder back to a String. 
      s = sb.ToString();

      // Display the String to the user.
      Console.WriteLine(s);  // "JEFFREY-Marc-RICHTER"
   }

获取对象的字符串表示:ToString

Microsoft在FCL中定义的许多类型都能同时识别几种格式。例如,DateTime类型支持用d标识短日期,用D标识长日期,用g标识常规,用M标识月日,用S标识排序,用Y标识年月。
所有内建值类型都支持C标识货币格式,用D标识十进制格式,用E标识科学记数法,用G标识常规模式,用N标识数字格式,用P标识百分数,X标识十六进制。

解析字符串来获取对象:Parse

能解析字符串的任何类型都提供了公共静态方法Parse。方法获取一个String并获取类型的实例。使用方法很简单

// 调用失败,因为解析的字符串包含空格
Int32 x= Int32.Pares(" 123",NumberStylesNone,null);

字符串作为最常用的对象,使用起来还是没有什么难度。有一些难度的听都没听说过,比如最后的安全字符串。努力吧。

《CLR via C#》---泛型

熟悉面向对象编程的开发人员都深谙这种编程方式的好处,其中一个好处就是代码重用。而今天讲的泛型是一种特殊的机制,它允许算法重用

泛型List类的设计者紧接在类名后添加了一个<T>,表明它操作的是一个未指定的数据类型。定义泛型类型或方法时,为类型指定的任何变量(比如T)都成为类型参数,在使用泛型时指定的数据类型称为类型实参

泛型为开发人员提供了以下优势:

  • 源代码保护

  • 类型安全

  • 更清晰的代码

  • 更佳的性能

建议就是能使用泛型的地方就不要使用非泛型类


请看以前写的文章

泛型约束

主要约束

有两个主要的约束Classstruct。其中,class约束像编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或数组类型都满足这个约束。struct约束类型实参是值类型,包括枚举在内任何值类型都满足这个约束。

    internal static class PrimaryConstraintOfClass<T> where T : class {
         public static void M() {
            T temp = null;    // Allowed because T must be a reference type
         }
      }
    
    internal static class PrimaryConstraintOfStruct<T> where T : struct {
         public static T Factory() {
            // Allowed because all value types implicitly 
            // have a public, parameterless constructor
            return new T();
         }
      }

次要约束和构造器约束

。。。


总结:

今天的内容比较无聊,但却很重要。以前的时候都是使用的比较多,像一些协变性 逆变性 约束都不是很清楚,其实写了这篇文章也不是很清楚,但总感觉记录了点什么。继续往下吧。等将来的某一天也许会回来继续编辑这篇文章。

《CLR via C#》---事件详解

是什么

本文讨论类型中定义的最后一种成员:事件
定义了时间成员的类型允许类型通知其他对象发生了特定的事情。
具体的说,定义了时间成员的类型能提供以下功能:

  • 方法能登记它对事件的关注

  • 方法能注销它对事件的关注

  • 事件发生时,登记了的方法将收到通知

CLR事件模型以委托为基础。委托是调用回调方法的一种类型安全的方式。

怎么用

本文我们将通过一个小栗子来学习一下事件怎么使用:

假定有个bolg平台可以订阅文章,用户A、B、C可以通过订阅或取消订阅来接收或者不接受此平台的文章。即,我是管理员,我在blog上发了一篇文章,那么订阅blog的用户就可以收到这篇文章了。

下面我们来实现这个功能,顺便来学习下事件的使用:

第一步:定义类型来容纳所有需要发送给事件通知者的附加信息

约定:这种类型应该从System.EventArgs派生,而且类名以EventArgs结束。

    internal class BlogEventArgs : EventArgs
    {
        private readonly String author, content;
        private DateTime tdate;

        public BlogEventArgs(string author,string content,DateTime date)
        {
            this.author = author;
            this.content = content;
            this.tdate = date;
        }

        public string Author { get { return author;} }
        public string Content { get { return content; } }
        public DateTime TDate { get { return tdate;} }
    }

第二步:定义事件成员

约定:事件使用event关键字。每个事件成员要指定以下内容:可访问性标识符;委托类型;以及名称。

    internal class BlogManager
    {
        public event EventHandler<BlogEventArgs> NewBlog;
    }

第三步:定义负责引发事件的方法来通知事件的登记对象

约定:类要定义一个受保护的虚方法。引发事件时,类及其派生类中的代码回调用该方法。方法只接受一个参数,即BlogEventArgs对象

    protected virtual void OnNewBlog(BlogEventArgs e)
        {
            EventHandler<BlogEventArgs> temp = Volatile.Read(ref NewBlog);
            if (temp!=null)
            {
                temp(this, e);
            }
        }

关于为什么要像上边这样写,只能说这样是线程安全的方式引发事件,其他方式可能会有线程方面的问题。

第四步:定义方法将输入转化为期望事件

BlogManager中,调用WriteNewBlog来指出写了一篇新博客

    public void WriteNewBlog(string author, string content, DateTime date)
        {
            BlogEventArgs e = new BlogEventArgs(author,content,date);
            OnNewBlog(e);
        }

第五步:设计侦听事件的类型

比如User类型想订阅或取消这个博客

    internal sealed class Reader
    {
        public Reader(BlogManager blogManager)
        {
            blogManager.NewBlog += blogManager_NewBlog;
        }

        void blogManager_NewBlog(object sender, BlogEventArgs e)
        {
            Console.WriteLine("读者已收到博客!");
            Console.WriteLine("作者:{0},内容:{1},发表时间:{2}",e.Author,e.Content,e.TDate.ToShortTimeString());
        }

        public void Unregister(BlogManager bmManager)
        {
            bmManager.NewBlog -= blogManager_NewBlog;
        }
    }
#### 最后一步使用起来

    class Program
    {
        static void Main(string[] args)
        {
            BlogManager bmManager = new BlogManager();
            Reader readerA  =new Reader(bmManager);
            bmManager.WriteNewBlog("1号作者", "这是我的第一篇文章", DateTime.Now);
            readerA.Unregister(bmManager);
            bmManager.WriteNewBlog("1号作者", "这是我的第二篇文章,应该收不到", DateTime.Now);
            Console.ReadKey();
        }
    }

运行

运行结果


总结

根据书本《CLR via C#》的小栗子自己更改了一点,希望对你有帮助。

《CLR via C#》读书笔记---属性

本章讨论属性,它允许源代码用简化语法来调用方法。CLR支持两种属性:无参属性 有参属性。在C#中称有参属性索引器

无参属性

面向对象设计和编程的重要原则之一就是数据封装,意味着类型的字段永远不应该公开,否则很容易因为不恰当的使用字段而破坏对象的状态。
下面这种使用方式是不好的:

public sealed class Employee{
    public String Name;
    public Int32 Age
}

Employee e = new Employee();
e.Name = "Jeffrey Richter";
e.Age = 45;
//下面这种方式很容易就破坏了对象的状态
e.Age = -5;

基于以上原因,强烈建议将所有字段都设为private.要允许用户或类型获取或设置状态信息就公开一个针对该用途的方法。封装了字段访问的方法通常称为访问器方法。

比如下面这样使用属性:

private sealed class Employee {
    private String m_Name; // prepended 'm_' to avoid conflict
    private Int32 m_Age;  // prepended 'm_' to avoid conflict

    public String Name {
        get { return (m_Name); }
        set { m_Name = value; } // 'value' identifies new value
    }

    public Int32 Age {
        get { return (m_Age); }
        set {
            if (value <= 0)    // 'value' identifies new value
                throw new ArgumentOutOfRangeException("value", "must be >0");
            m_Age = value;
        }
    }
}

emp.Name = "Jeffrey Richter";
emp.Age = 45;       // Updates the age
Console.WriteLine("Employee info: Name = {0}, Age = {1}", emp.Name, emp.Age);

e.Age = -5; // 会抛出`ArgumentOutOfRangeException`异常
Int32 EmployeeAge = e.Age ; 

注意:我相信大部分的初学者在开始之初都讲不出字段属性的区别,现在我可以稍微尝试着说下了。字段经常用来封装一些不经常修改的内容,建议将字段设为private,而属性一定要将取值get设值set想象成两个方法,虽然C#简化了这种语法,但编译器在后台仍然会在指定的属性名之前添加set_get_前缀来生成方法名,更适合用来封装那些可能发生变化的内容。可以将属性看成智能字段,可以设置只读属性只写属性

自动实现的属性

public String Name {get;set;}

声明属性而不提供get/set方法的实现,C#会自动为你声明一个私有字段。实现get/set方法。

合理定义属性

  • 属性可以只读或只写,字段访问总是可读或可写的(readonly字段在构造器中可写)。如果定义属性,最好同时位它提供getset访问方法。

  • 属性方法可抛出异常,字段不会

  • 属性不能作为outref参数传递给方法

  • 属性可能花较长时间完成,字段访问总是立即完成

  • 连续多次访问,属性方法可能每次返回不同的值,而字段每次都是相同的值

对象和集合初始化器

对象初始化语法:

Employee e = new Employee(){ Name = "Jeff" , Age = 45 };

如果想调用的本来就是一个无参构造器,C#还允许省略括号

下面演示集合初始化器,下面构造一个ClassRoom对象,并初始化Students集合

Classroom classroom = new Classroom{
    Student = { "Jeff" , "Kristin" , "Aidan"}
};

使用起来还是非常方便的,编译器在后台默认调用集合的Add方法,把对象添加到集合中。

匿名类型

利用C#的匿名类型功能,可以很简洁的语法来自动声明不可变的元组类型。

 var o1 = new { Name = "Jeff", Year = 1964 };

 // Display the properties on the console:
 Console.WriteLine("Name={0}, Year={1}", o1.Name, o1.Year);

匿名类型经常与LINQ联合使用

有参属性

在C#中有参属性被称为索引器。C#是用数组风格的语法来公开有参属性,换句话说索引器可以看成是C#对[]操作符的重载

下面演示一个BitArray类,它允许哦那个数组风格的语法来索引一组二进制位

internal sealed class BitArray {
//容纳了一个二进制位的私有字节数组
// Private array of bytes that hold the bits
private Byte[] m_byteArray;
private Int32 m_numBits;

// Constructor that allocates the byte array and sets all bits to 0
//分配字节数组,并将所有为初始为0
public BitArray(Int32 numBits) {
    // Validate arguments first.
    if (numBits <= 0)
        throw new ArgumentOutOfRangeException("numBits must be > 0");

    // Save the number of bits.
    m_numBits = numBits;

    // Allocate the bytes for the bit array.
    m_byteArray = new Byte[(m_numBits + 7) / 8];
}


// This is the indexer.
// 这是索引器
public Boolean this[Int32 bitPos] {
    // 这是索引器的get方法
    get {
        // Validate arguments first
        if ((bitPos < 0) || (bitPos >= m_numBits))
            throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);

        // Return the state of the indexed bit.
        return ((m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0);
    }

    // 索引器的set访问器方法
    set {
        if ((bitPos < 0) || (bitPos >= m_numBits))
            throw new ArgumentOutOfRangeException("bitPos", "bitPos must be between 0 and " + m_numBits);

        if (value) {
            // Turn the indexed bit on.
            m_byteArray[bitPos / 8] = (Byte)
               (m_byteArray[bitPos / 8] | (1 << (bitPos % 8)));
        } else {
            // Turn the indexed bit off.
            m_byteArray[bitPos / 8] = (Byte)
               (m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8)));
        }
    }
}
}


private static void BitArrayTest() {
    // Allocate a BitArray that can hold 14 bits.
    // 构造一个数组
    BitArray ba = new BitArray(14);

    // Turn all the even-numbered bits on by calling the set accessor.
    for (Int32 x = 0; x < 14; x++) {
        //这里实际是调用的set方法,将偶数为设为true
        ba[x] = (x % 2 == 0);
    }

    // Show the state of all the bits by calling the get accessor.
    for (Int32 x = 0; x < 14; x++) {
        // 这里调用了get方法,显示状态。
        Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
    }
}

C#使用this[...]作为表达索引器的语法,如上面的

public Boolean this[Int32 bitpos]

代表set时,传入一个index的索引,get时返回一个Boolean类型


总结

今天的内容还是比较有趣的, 搞懂了一直以来困惑的字段属性的区别。复习了对象初始化器、集合初始化器、匿名类型等等,最后学习了下有参属性即索引器。以前在做题的时候遇到过,现在也清楚多了。接下来到了事件了哈哈,做个小栗子一起学习下吧。

《CLR via C#》读书笔记--常量、字段、方法和参数

常量

常量是值从不变化的符号。定义常量符号时,它的值必须能在编译时确定。确定后,编译器将唱两只保存在程序集元数据中。使用const关键字声明常量。由于常量值从不变化,所以常量总是被视为类型定义的一部分。换言之,常量总是被视为静态成员,而不是实例成员。常量的值直接潜入代码,在运行时不需要为常量分配任何内存。

字段

字段是一种数据成员,其中容纳了一个值类型的实例或者一个对引用类型的引用。由于字段存储在动态类型中,所以它们的值在运行时才能获取。字段还解决了常量存在的版本控制问题。字段可以是任何数据类型。

CLR支持readonly字段和 read/write字段,大多数字段是read/write字段,意味着在代码执行过程中,字段值可多次改变,但readonly字段只能在构造器方法中写入。

提示:当某个字段是引用类型,并且该字段被标记为readonly时,不可改变的是引用,而非字段引用的对象。

public sealed class AType {
  // InvalidChars must always refer to the same array object
  public static readonly Char[] InvalidChars = new Char[] { 'A', 'B', 'C' };
}

public sealed class AnotherType {
  public static void M() {
     // The lines below are legal, compile, and successfully 
     // change the characters in the InvalidChars array
    //编译成功没什么问题
     AType.InvalidChars[0] = 'X';
     AType.InvalidChars[1] = 'Y';
     AType.InvalidChars[2] = 'Z';

     // The line below is illegal and will not compile because 
     // what InvalidChars refers to cannot be changed
    // 非法的,无法通过编译
     //AType.InvalidChars = new Char[] { 'X', 'Y', 'Z' };
  }
}

方法

实例构造器和类

构造器是将类型的实例初始化为良好状态的特殊方法。构造器在方法定义源数据表中始终叫做.ctor(vs构造函数的代码段就是ctor),创建引用类型的实例时,首先为实例的数据字段分配内存,然后初始化对象的附加字段,最后调用类型的实例构造器来设置对象的初始状态。

构造引用类型的对象是,在调用实力构造器之前,为对象分配的内存总总是先被归零,没有被构造器显式重写的所有字段都保证获得0null

实例构造器永远不能被继承,一个类型可以定义多个实力构造器,每个构造器都必须有不同的签名。

C#用简单的语法在构造引用类型的实例时初始化类型定义中的字段

internal sealed class SomeType {
    private Int32 m_x = 5;
}

SomeType的构造器先把5存到m_x,在调用基类的构造器。

如果有几个以初始化的实力字段和许多重载的构造器方法,可以创建单个构造器来执行这些公共的初始化,然后让其他构造器都显式调用这个公共的构造器,这样能减少代码。

internal sealed class SomeType {
    // Do not explicitly initialize the fields here
    private Int32 m_x;
    private String m_s;
    private Double m_d;
    private Byte m_b;

    // This method MUST be called by all constructors.
    private void SetFieldDefaults() {
        m_x = 5;
        m_s = "Hi there";
        m_d = 3.14159;
        m_b = 0xff;
    }

    // This constructor sets all fields to their default.
    public SomeType() {
        SetFieldDefaults();
    }

    // This constructor sets all fields to their default, then changes m_x.
    public SomeType(Int32 x) {
        SetFieldDefaults();
        m_x = x;
    }

    // This constructor sets all fields to their default, then changes m_s.
    public SomeType(String s) {
        SetFieldDefaults();
        m_s = s;
    }

    // This constructor sets all fields to their default, then changes m_x & m_s.
    public SomeType(Int32 x, String s) {
        SetFieldDefaults();
        m_x = x;
        m_s = s;
    }
}

实例构造器和结构

CLR总是允许创建值类型的实例,并且没有办法阻止值类型的实例化。值类型其实并不需要定义构造器,但CLR确实允许为值类型定义构造器。但必须显式调用才能执行。结构不能包含显式的无参构造器。没有无参构造器,值类型的字段总是被初始化为0null

扩展方法

要想定义自己的扩展方法,只需要在第一个参数前面添加this关键字。
规则和原则

  • C#只支持扩展方法,不支持扩展属性、扩展事件、扩展操作符。

  • 扩展方法必须在非泛型的静态类中声明,类型没有限制,只有第一个参数前面能用this关键字标记

  • 编译器在查找扩展方法时,要求静态类文件本身必须必有文件作用域,扩展方法必须在顶级静态类中作用,不鞥你在嵌套类中定义

  • 多个静态类可以定义相同的扩展方法,编译器检测到多个扩展方法,会提示方法调用不明确

  • 扩展方法存在版本控制问题,将来Microsoft添加和你一样的扩展方法,程序就会有不同的行为。

来一个扩展方法栗子:

internal static class StringBuilderExtensions {
public static Int32 IndexOf(this StringBuilder sb, Char value) {
    for (Int32 index = 0; index < sb.Length; index++)
        if (sb[index] == value) return index;
    return -1;
}
}

参数

可选参数和命名参数

private static Int32 s_n = 0;

public static void Go() {

  ImplicitlyTypedLocalVariables();

  // 1. Same as: M(9, "A", default(DateTime), new Guid());
  // 直接调用M
  M();

  // 2. Same as: M(8, "X", default(DateTime), new Guid());
  // 前两个参数指定,后两个是默认值
  M(8, "X");

  // 3. Same as: M(5, "A", DateTime.Now, Guid.NewGuid());
  // 使用明明参数指定值
  M(5, guid: Guid.NewGuid(), dt: DateTime.Now);

  // 4. Same as: M(0, "1", default(DateTime), new Guid());
  // s_n先传入0 ,然后加1,把1传入,再加1 ,此时s_n是 2
  M(s_n++, s_n++.ToString());

  // 5. Same as: String t1 = "2"; Int32 t2 = 3; 
  //             M(t2, t1, default(DateTime), new Guid());
  // 2传入 s。然后加1,传入x
  M(s: (s_n++).ToString(), x: s_n++);
 }
  private static void M(Int32 x = 9, String s = "A",
  DateTime dt = default(DateTime), Guid guid = new Guid()) {

  Console.WriteLine("x={0}, s={1}, dt={2}, guid={3}", x, s, dt, guid);
}

规则和原则

  • 可为方法、构造器方法和有参属性的参数指定默认值。

  • 有默认值的参数必须放在没有默认值的所有参数之后。换言之,一旦定义了有默认值的参数,它右边的所有参数也必须有默认值。

  • 默认值必须是编译时能确定的常量值

  • 不要重命名参数变量,否则任何顶用着已传参数名的方式传递实参代码也必须得修改

  • 如果方法从模块外部调用,更改参数的默认的默认值具有潜在的危险性

  • 如果参数用refout关键字进行了标识,就不能设置默认值

隐式类型的局部变量

var关键字的真正价值就是让程序员少打几个字。

以传引用的方式向方法传递参数

即C#中refout关键字。以前用的不做,现在看来得经常使用。
先看一下out的栗子

public static void Main()
{
    Int32 X;    //没有初始化
    GetValue(out x); //
    Console.WriteLine(x); //显式10
}

private static void GetValue(out Int32 v)
{
    v = 10; // 在这里面必须初始化
}

在看一个ref的栗子

public static void Main() {
        Int32 x = 5;         // x is 初始化的字段
        AddVal(ref x);        // x must be initialized.
        Console.WriteLine(x); // Displays "15"
    }

    private static void AddVal(ref Int32 v) {
        v += 10;  // This method can use the initialized value in v.这方法可以改变已初始化的值
    }

为值类型使用outref,等同于以传值的方式传递引用类型。
暂时就介绍上面两种使用refout的方式。

向方法传递可变数量的参数

通过使用param关键字完成

private static Int32 Add(params Int32[] values) {
  // NOTE: it is possible to pass the 'values' 
  // array to other methods if you want to.

  Int32 sum = 0;
  for (Int32 x = 0; x < values.Length; x++)
     sum += values[x];
  return sum;
}
Console.WriteLine(Add(1, 2, 3, 4, 5));//显式15

参数和返回类型的设计规范

声明方法的参数类型时,应尽量指定最弱的类型,宁愿要结构也不要基类。而返回类型要声明为最强的类型


总结

今天的依然是非常无聊但却非常基础的文章,比如扩展方法refout默认参数等等这些都在实际编程中是经常用到的,必须要掌握牢固,今天一个微软亚洲研究院大数据系列讲座开课了,努力坚持学完。