C# – 特性(OOP)

与 Java 不同, C# 中的方法不是默认为虚函数。所以如果子类需要覆盖 C# 中的普通方法话,可以使用 new 关键字覆盖。这个方法与基类中的方法没有任何的关联

接口

如果类实现多个接口的签名相同的方法,可以使用显示接口实现(EIMI)来分别实现。类中的方法默认为 private,但不能显式指定。如果需要调用的话需要将对象转换为对应的接口才能调用对应的方法。Int32 实现了 IConvertible 接口(包含了 ToSingleToType 等一些转换的方法),然而 Int32 的对象必须转换为 IConvertible 才能调用这些实现的方法。但也有缺点:

  • 值类型在转换为接口时装箱
  • 派生类无法调用。所以可以在基类中再定义一个虚方法,然后派生类覆盖并再其中调用基类的接口方法

显式接口实现是再不使用泛型的接口版本时提供一个类型安全的比较版本。同时不进行装箱操作

interface Printer
    {
        void Print();
    }
    interface ThreeDPrinter
    {
        void Print();
    }
    class MyNewPrinter : Printer, ThreeDPrinter
    {
        //public void Print()
        //{
        //    Console.WriteLine("新的打印机的打印功能");
        //}
        //使用显示接口实现
        void ThreeDPrinter.Print()
        {
            Console.WriteLine("3D打印机的打印功能");
        }
        void Printer.Print()
        {
            Console.WriteLine("普通打印机的打印功能");
        }
    }
    class ExplicitInterfaceImpl
    {
        static void Main(string[] args)
        {
            MyNewPrinter my = new MyNewPrinter();
            ((ThreeDPrinter)my).Print();
            ((Printer)my).Print();
        }
    }

 

如果子类需要使用基类的显式实现的接口实现,则需要在子类中非显式实现的接口内部调用基类的接口方法。但这样会导致无限递归。

class Base : IComparable
        {
            Int32 IComparable.CompareTo(Object obj)
            {
                Console.WriteLine("基类的ICompareable.CompareTo");
                return 0;
            }
        }
        class Derived : Base, IComparable
        {
            public Int32 CompareTo(Object o)
            {
                Console.WriteLine("子类的虚CompareTo");
                IComparable cmp = this;
                cmp.CompareTo(o);
                return 0;
            }
        }
        public static void Main()
        {
            Base b = new Base();
            Derived d = new Derived();
            d.CompareTo(b);
        }

 

解决方法是在基类中实现一个虚方法,而显式实现的接口方法则调用这个虚方法,子类中覆盖这个虚方法然后调用基类的方法。

class Base : IComparable
        {
            Int32 IComparable.CompareTo(Object obj)
            {
                Console.WriteLine("基类的ICompareable.CompareTo");
                return CompareTo(obj);
            }
            public virtual Int32 CompareTo(Object o)
            {
                Console.WriteLine("基类的虚CompareTo");
                return 0;
            }
        }
        class Derived : Base, IComparable
        {
            public override Int32 CompareTo(Object o)
            {
                Console.WriteLine("子类的虚CompareTo");
                IComparable cmp = this;
                cmp.CompareTo(o);
                return 0;
            }
        }

 

常量和字段

  • 常量由 const 指明, 同时 const 始终为 staticconst 必须是值类型。常量的值直接嵌入代码所以运行时不需要分配内存,所以也就不能获取它的地址,也就不能通过引用进行转递。
  • 字段可以为实例字段和静态字段。CLR 支持 read/write 字段,可以通过使用 readonly 关键字。这种字段可以在构造方法中写入,CLR 保证字段不会在构造方法以外的地方被修改,但是使用反射是可以修改 readonly 字段。如果 readonly 的字段是引用类型,那么这个字段本身是不可修改,但内容可以通过它的方法进行修改。
  • CLR 本身没有提供将实例方法、参数声明为常量(C++ 可以使用 const 限定),因为这可能会影响性能。

构造方法

可以分为静态构造方法和实例构造方法。在极少数的情况下,类实例化的时候可以不用调用实例构造方法,例如反序列化的时候,使用 System.Runtime.Serialization.FormatterServices 类型的 GetUninitializedObject 或者 GetSafeUninitializedObject 方法为对象分配内存。

值类型的构造器的注意事项:

  • 值类型的默认的无参数构造方法不能被显式定义,因为编译器并不会生成值类型的无参数构造方法(为了运行时性能)。
  • 定义值类型的构造函数时,必须确保所有的字段被初始化
  • 值类型的 this 引用不是只读的,可以直接将 new 创建的另一个实例赋值给 this,而引用类型则不行。
  • 值类型的实例字段不能进行内联初始化。但是静态字段可以。

静态构造方法也称类型构造器, CLR 保证这个方法只会被调用一次,当有多个线程需要调用它时,调用的线程会获取一个互斥同步锁,其他线程被阻塞,所以适用于单例模式。但是值类型的静态构造方法可能不会被调用。

struct SomeValueType{
    static SomeValueType(){
        Console.WriteLine("这句话永远不显示");
    }
}

使用 System.Runtime.CompilerServices.RuntimeHelpers 中的 RunClassConstructor 方法可以动态调用类型的静态构造方法。

运算符重载

  • +、-、!、~、++、--、true、false,这些一元运算符可以进行重载。
  • +, -, *, /, %, &, |, ^, <<, >>,这些二元运算符可以进行重载。
  • ==, !=, <, >, <=, >=,比较运算符可以进行重载(但是请参阅此表后面的备注)。
  • &&, || 条件逻辑运算符无法进行重载,但是它们使用 & 和 |(可以进行重载)来计算。
  • [],数组索引运算符无法进行重载,但是可以定义索引器。
  • (T)x 强制转换运算符无法进行重载,但是可以定义新转换运算符
  • +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=,赋值运算符无法进行重载,但是 +=(举例)使用 +(可以进行重载)来计算。
  • =、.、?:、??、->、=>、f(x)、as、checked、unchecked、default、delegate、is、new、sizeof、typeof,这些运算符无法进行重载。

注意事项

  • 重载的运算符都必须是 static, 参见MSDN 博客
  • 前置和后置自增/自减运算:对于引用类型,不建议重载自增/自减运算符,因为无论是前置还是后置自增/自减运算符的语义是一样的无法单独控制(和 C++ 通过函数参数来确定行为不一样);对于值类型,编译器会自动处理前置、后置的语义。
class Test
        {
            Int32 m_data = 0;
            public static Test operator++(Test test)
            {
                ++test.m_data;
                return test;
            }
            public override String ToString()
            {
                return "[Test " + m_data + "]";
            }
        }
        struct Test2
        {
            Int32 m_data;
            public static Test2 operator++(Test2 test)
            {
                ++test.m_data;
                return test;
            }
            public override String ToString()
            {
                return "[Test2 " + m_data + "]";
            }
        }
        public static void Main()
        {
            Test t1 = new Test();
            Console.WriteLine(++t1);
            Console.WriteLine(t1++);
            Console.WriteLine("-----");
            Test2 t2 = new Test2();
            Console.WriteLine(++t2);
            Console.WriteLine(t2++);
            Console.WriteLine(t2);
        }
//得到的输出:
/*
[Test 1]
[Test 2]
-----
[Test2 1]
[Test2 1]
[Test2 2]
*/

可以看见 Test 类型没有处理语义的不同。但是值类型可以。可以看到 CIL 代码:

IL_0030:  initobj    Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2
  IL_0036:  ldloc.1
  IL_0037:  call       valuetype Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2 Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2::op_Increment(valuetype Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2)
  IL_003c:  dup
  IL_003d:  stloc.1
  IL_003e:  box        Part3.A
  IL_003d:  stloc.1
  IL_003e:  box        Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2
  IL_0043:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0048:  nop
  IL_0049:  ldloc.1
  IL_004a:  dup
  IL_004b:  call       valuetype Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2 Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2::op_Increment(valuetype Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2)
  IL_0050:  stloc.1
  IL_0051:  box        Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2
  IL_0056:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_005b:  nop
  IL_005c:  ldloc.1
  IL_005d:  box        Part3.Advanced_CSharp_Programming_Structure.OperatorOverload/Test2
  IL_0062:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0067:  nop
  IL_0068:  ret
} // end of method OperatorOverload::Main

注意在 IL_004a 的时候调用了 dup 指令,用于复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上;而我显示即 IL_0056 调用的是使用 stloc.1(IL_0050) 从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中的副本(已经通过 dup 指令复制)。而前置自增并没有做这样的处理。

类型转换

  • 隐式转换:public static implicit operator ( xxx), 从 InputType 构造为新的 OutputType 对象,这个类似于 C++ 中之含有一个参数的公共构造函数的作用
  • 显式转换:public static explicit operator ( xxx),允许将 InputType 强制转换为 OutputType 类型的对象。类似于 C++ 中 operator () const {...}

扩展方法

定义在非泛型静态类中,扩展方法必须至少有一个参数,且用 this 修饰。

分部方法

实现部分和声明部分需要使用关键字 partial 关键字标记。分部方法的返回值必须是 void,任何参数参数不能有 out,但是可以有 ref、泛型方法、实例或者静态方法,也可以标记为 unsafe。分部方法总是被隐式标记为 private。声明的分部方法可以没有实现,但是被用在委托上时,会出现 CS0762 的错误。

参数

C# 可以通过 xxx: 为参数名为 xxx 的参数指定值。参数可以有默认值,且在编译可以被计算。 default 和 new 可以表达这个意思。不要修改默认参数的值,建议将默认的值作为一个 0/null 作为一个哨兵使用。ref、out 不能指定默认值。ref、out 必须b保持类型一致。使用类默认参数的位置应用了 System.Runtime.CompilerServices.OptionalAttribute 特性以及 DefaultParameterValueAttribute 特性,向后者的构造器传递了默认值。params T[] xxx,可以定义可变参数,类型为 T。

属性

C# 支持自动属性(AIP),不需要自己定义私有实例成员。但时这也会导致一些问题:

  • 序列化引擎会把字段名加入序列化流中,而自动生成的私有实例成员的名称依赖于编译器。
  • 不能在 get/set 上进行调试。

对于普通属性编译器会在 CIL 中生成对应的 get_xxx/set_xxx 代码。结合 var 可以声明匿名类型,非常适合 LINQ 的使用。但有几点需要注意:

  • 不能作为 ref/out 的参数
  • 属性并没有明确返回的时浅拷贝还是深拷贝的副本
String Name="Grant";
DateTime dt = DateTime.Now;
var o2 = new {Name, dt.Year};

o2 的包含属性:Name -> "Grant", Year -> DateTime.Now。此时编译器会生成一个匿名类。

如果需要返回多个值也可以使用元组 System.Tuple:

public static Tuple<Int32, Int32> MinMax(Int32 a, Int32 b)
        {
            return new Tuple<int, int>(Math.Min(a, b), Math.Max(a, b));
        }
        public static void Main()
        {
            var minMax = MinMax(50, 33);
            Console.WriteLine("Min:{0}, Max:{1}", minMax.Item1, minMax.Item2);
        }

属性为 Item1~Item7(默认最多7个,但是可以将另一个 Tuple 作为元素)。除了使用 new 运算符,还可使用 Tuple 的静态工厂方法 Create 创建元组对象。

另一个方法是使用 System.Dyanmic.ExpandoObject 创建一个动态的类型,这个类型可以表示一个键值对并且可以进行 foreach 迭代,而迭代的结果就是键值对类型(需要强制转换)。

dynamic e = new System.Dynamic.ExpandoObject();
            e.x = 6;
            e.y = "Jeff";
            e.z = null;
            foreach(var v in (IDictionary<String, Object>)e)
            {
                Console.WriteLine("Key={0}, V={1}", v.Key, v.Value);
            }

有参属性

类似于索引器,一般没有名称吗,但是在 CIL 中会生成 get_Item 和 set_Item, 所以如果需要避免 Item 属性的重复,可以使用 CLI 不兼容的特性 System.Runtime.CompileServices.IndexerName 指定在 CIL 中的名称。以下是示例:

sealed class BitArray
    {
        private Byte[] m_byteArray;
        private Int32 m_numBits;
        public BitArray(Int32 numBits)
        {
            if (numBits <= 0)
                throw new ArgumentOutOfRangeException("numBits must be > 0");
            this.m_numBits = numBits;
            m_byteArray = new Byte[numBits];
        }
        [System.Runtime.CompilerServices.IndexerNameAttribute("Bit")]
        public Boolean this[Int32 bitPos]
        {
            get
            {
                if ((bitPos < 0) || (bitPos >= m_numBits))
                    throw new ArgumentOutOfRangeException("bitPos");
                return (m_byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0;//每一位保存一个状态
            }
            set
            {
                if ((bitPos < 0) || (bitPos >= m_numBits))
                    throw new ArgumentOutOfRangeException("bitPos", bitPos.ToString());
                if(value)
                {
                    //将指定的索引设置为true
                    m_byteArray[bitPos / 8] = (Byte)(m_byteArray[bitPos / 8] | (1 << (bitPos % 8)));
                }
                else
                {
                    m_byteArray[bitPos / 8] = (Byte)(m_byteArray[bitPos / 8] & ~(1 << (bitPos % 8)));
                }
            }
        }
        public static void Main()
        {
            List<int> s = new List<int>();
            BitArray ba = new BitArray(14);
            for(Int32 x=0;x<14;++x)
            {
                ba[x] = (x % 2 == 0);
            }
            for(Int32 x=0;x<14;++x) 
            {
                Console.WriteLine("Bit " + x + " is " + (ba[x] ? "On" : "Off"));
            }
        }
    }

事件

从委托开始

委托默认从 System.Delegate, 而如果需要绑定对各方法的话就需要使用 System.MulticastDelegate (多播委托)。其中定义了 CombineRemove 方法将要绑定的方法添加、移除。在多播委托中定义了对象数组用来保存方法。回调委托的时候所有的中间返回值会被忽略,使用最后一个的返回作为 Invoke 的返回值。如果多播委托中的中间某一个方法抛出了异常,则这个过程就会中断。但是 GetInvocationList 可以返回一个 Delegate 对象数组,可以控制调用过程。

注意:使用 lambda 表达式的时候被捕捉的对象的生命期延长了(闭包自动转换为一个匿名类)。

委托与反射结合可以动态创建一个委托:

 private static Object Add(Int32 lhs, Int32 rhs)
        {
            return lhs + rhs;
        }
        delegate Object BinaryOpt(Int32 lhs, Int32 rhs);
        public static void Main()
        {
            MethodInfo mi = typeof(DynamicDelegate).GetTypeInfo().GetDeclaredMethod("Add");
            Delegate d = mi.CreateDelegate(typeof(BinaryOpt));
            Int32 res = (Int32)d.DynamicInvoke(20,10);
            Int32 res2 = (Int32)d.DynamicInvoke(new Object[] { 20,100 });//千万不要使用 Int32 ,应该使用 Object
            Console.WriteLine(res);
            Console.WriteLine(res2);
        }

注意,如果绑定的是实例方法还需要在 CreateDelegate 的第二个参数指定对象实例。

事件

事件是委托的安全的访问者,可以将 event 定义的事件公开给使用者。事件中使用了委托,而且使用了 Interlock 来保障线程安全。event EventHandler OnChange; 最简定义一个事件。

泛型

泛型是 CLR 提供的一种特殊机制,它支持另一种形式的代码重用,即算法重用。泛型容器类型安全,不需要进行强制类型转换(装/拆箱),减轻了垃圾回收的压力。

使用反射可以创建开放类型(存在一个泛型参数未指定)和封闭类型(所有泛型参数已指定)。由于泛型类/接口仍然是 Type 的一个实例。使用 Activator.CreateInstance(Type type) 创建一个封闭类型的实例。从开放类型创建是不允许的。使用 Type.MakeGenericType 方法将类型参数的 Type 数组传入,返回一个封闭类型的 Type 实例。

Type unclosed = typeof(Dictionary<,>);//开放类型
//Exception:
//Object obj1 = Activator.CreateInstance(unclosed);
Type closed = typeof(Dictionary<Int32, Boolean>);//封闭类型
Dictionary<Int32, Boolean> obj2 = (Dictionary<Int32, Boolean>)Activator.CreateInstance(closed);

泛型类型委托

委托支持返回类型协变、参数逆变。

private delegate TResult DelegateDemo<out TResult, in TParameter>(TParameter parameter);
DelegateDemo<Object, ArgumentException> del =
                new DelegateDemo<Exception, Exception>((Exception obj) => obj);//合法的

del 的 Invoke 方法的除名称签名仍然保持不变,与定义委托的签名一致。对于值类型,引用之间的转换是不存在的,所以可变性用处不大。对于使用 ref、out 限定的参数,类型参数必须是不变的(协变/逆变都是可变的)。

类的类型参数约束

  • 主要约束:代表非密封的类型,不能是特殊的类型:System.ObjectSystem.ArraySystem.DelegateSystem.ValueTypeSystem.Enum 或者 System.Voidstruct 或者 class 可以指定类型为值类型或者引用类型。
  • 次要约束:代表接口类型,类型参数必须是指定类型或者它的子类
  • 构造器约束:new() 指定了 必须有公共无参数构造函数,值类型一定满足 new()

通过使用静态构造函数检查类型参数:如果参数不是枚举类型,就会出现 System.TypeInitializationException 异常,内部的 InnerException 属性就是静态构造构造器抛出的异常。

public class ClassConstrainTypeParameter<T>
{
    static ClassConstrainTypeParameter()
     {
        var typeinfo = typeof(T);
        if(!typeinfo.IsEnum)
        {
            throw new ArgumentException("非法的类型参数:" + typeinfo.Name);
        }
    }
}
public static void Main()
{
    ClassConstrainTypeParameter<Int32> tint32 = new ClassConstrainTypeParameter<int>();
}

C++ 和 Java 的泛型

为了限制生成的封闭类型的代码大小,Java 使用了类型擦除技术,擦出原有类型信息,JVM 对于 泛型一无所知。Java 的编译器仅仅在泛型方法调用的位置插入类型检查相关的代码以此确保类型转换安全。但也会出现一些问题:

import java.util.*;
public class type_erasure{
public static void main(String[] args)
{
GT<Integer> gti = new GT<Integer>();
gti.var = 1;
GT<Double> gtd = new GT<Double>();
gtd.var = 2;
System.out.println(gti.var);
}
}
class GT<T>{
public static int var = 0;
public void nothing(T x){}
}
//输出是2

而在 C# 中,泛型是 CLR 支持的功能属于运行时级别的东西,所以 C# 不会进行类型擦除。在 IL 中还是可以看到类型参数的类型信息。JIT 编译器仅仅会生成一份泛型方法的实现。即使是跨应用程序边界的不同的泛型方法也只会关联到一份在内存中的方法表对应的方法实现。C# 使用了类型具体化技术,将类型参数作为第一(类)公民(First-class Citizen)对待。

In programming language design, a first-class citizen (also object, entity, or value) in a given programming language is an entity which supports all the operations generally available to other entities. These operations typically include being passed as a parameter, returned from a function, and assigned to a variable. – Wikipedia

意思是说,第一类公民、第一类对象(不特指面向对象里的"对象”)、第一类实体、第一类值(这些概念都是一个,只是叫法不同)是支持其他实体所有操作的实体。这里有两个地方要展开:

  1. 实体与其他实体:
  2. 通常实体是指各种各样的数据类型和值,比如对象、类、函数、字面量等,一般讨论都是指函数是不是第一类对象(first-class object)

  3. 操作:

这些实体所具有的操作有:可以作为变量或者数据结构存储、可以作为参数传递给方法/函数、可以作为返回值从函数/方法返回、可以在运行期创建和有固有身份

“固有身份”是指实体有内部表示,而不是根据名字来识别,比如匿名函数,还可以通过赋值叫任何名字。大部分语言的基本类型的数值(int, float)等都是第一类对象;但是数组不一定,比如C中的数组,作为函数参数时,传递的是第一个元素的地址,同时还丢失了数组长度信息。对于大多数的动态语言,函数/方法都是第一类对象,比如Python,但是Ruby不是,因为不能返回一个方法。第一类函数对函数式编程语言来说是必须的。

C++ 是不需要进行类型擦除,编译器遇到不同的泛型类就会生成机器码。是典型的 Code specialization实现。

参考 & 引用