.NET学习笔记(五) ——类型设计(下)

好久没有写BLOG了,BLOG一直有点问题。最近一段时间也有点堕落,书看的不多,上个月底 一直在看关于 类之间的继承,多态 。虚方法的继承,隐藏,重写,方法表,内存结构等。还在CSDN上看了不少帖子。这些东西有空还是会总结一下的。

新年了,希望有新的气象。有好多事都会有大的变化。目标,计划都有了,需要的就是自己不断的坚持,好好努力,按自己的路线走下 去。也祝大家新年快乐把。

———————————————————————————————————————————–

(接上篇)

疑问:基元类型为什么不和Decimal一样提供ToXXX()方法呢?他内部是否实现了这些方法?

解答:通过用Reflector,我看了下他们

 

1. Int32里是类似下面的方法,可以发现实际是调用了Convert.ToInt16里的实现 并且是受保护的方法。内部也没有转换操作符 。
short   IConvertible.ToInt16 (IFormatProvider   provider) 
{ 
        return   Convert.ToInt16(this.m_value); 
} 
2. decimal里面也与这样的受保护的实现方法
short   IConvertible.ToInt16 (IFormatProvider   provider) 
{ 
        return   Convert.ToInt16(this); 
} 

但也还有如下的   静态方法。

public   static   short   ToInt16(decimal   value) 
{ 
        int   num   =   ToInt32(value); 
        if   ((num   <   -32768)   ¦ ¦   (num   >   0x7fff)) 
        { 
                throw   new   OverflowException(Environment.GetResourceString ("Overflow_Int16")); 
        } 
        return   (short)    num; 
} 

看来因为类型都继承了接口,内部是必须实现接口的,但接口都是调用Conver类中的实现。
而对于基元类型,没有定义 ToXXX()方法,所以只能通过调用Conver类中的方法。
对于其他如Decimal类型,因为内部提供了公有静态方法,所以可以直接通过 类型调用ToXXX()方法实现。

 

3. 在看下Convert中的实现,实际他是调用decimal内部实现

public   static   short   ToInt16(decimal   value) 
{ 
        return   decimal.ToInt16 (decimal.Round (value,   0)); 
}  
4. 而对于那些内部没有实现ToXXX方法的,是在Convert中实现的。
public   static   short   ToInt16(int   value) 
{ 
        if   ((value   <   -32768)   ¦ ¦   (value   >   0x7fff)) 
        { 
                throw   new   OverflowException(Environment.GetResourceString ("Overflow_Int16")); 
        } 
        return   (short)    value; 
} 
  1. 看完上面基本明白了, 对IConvertible接口方法的实现,都是调用Convert类中的方法。
  2. 基元类型 的转换的实现基本都在Convert里面,而内部没有实现,所以要通过Convert实现转换。
  3. 对于内部实现了的ToXXX()方法的(比如 Decimal),我们可以直接调用此静态方法,也可以通过Convert的方法(其实他也是调用内部实现了的ToXXX()方法)
  4. 对于采用 强制类型转换,基元类型会自动产生IL指令,而其他类型则调用转换操作符方法。

关于为什么不在各自类型中直接实现IConvertible接口,直接提供接口调用,而是在Convert类中提供了方法。我觉得是因为接口是引 用类型,将一个未装箱的值类型实例转型为一个该实例实现的接口类型也需要对该实例进行装箱。如果直接使用接口,会因为装箱导致性 能损失。而使用Convert类来实现,就解决了这个问题。可能会有人问那为什么要实现IConvertible接口,这个是因为接口定义了一组功能 ,要实现这些功能就必须实现这些接口。所以继承这些接口是绝对没问题的。而提供Conver类应该是为了优化性能。对于decimal,实现方 法没有放在Convert里,而是放在自己类型中的Toxxx()方法中的,这个我想应该是为了方便使用,通过Conver或直接用decimal都可以调用 。

以上是我的理解,请大家指教

 

 

五 方法的参数

 

前面我门谈到了类型中的方法,方法包括了修饰符,返回类型,方法名字和参数。这里我们谈谈参数。说到参数,大家第一个想到的例 子或许就是C语言里面交换两个数字。什么值传递,地址传递,当时听到老师讲,实在是让我头晕。到了后来,C++中为了解决这个方法参 数传递又引入了引用。当然现在对于这些已经有了比较清楚的理解,也通过前面的学习,对不同对象的分配方式有了了解,所以如果从内 存分配模型上来看更好理解。

在.NET平台路CLR里方法的参数默认都是值传递的。对于引用类型,传递时传的是指向对象的引用(引用本身是按值传递的),对于值 传递,传递给方法的是值类型实例的一个拷贝,所以直接没有关系。在方法类修改形参数是不回影响实参。而对于传引用,实参和形参指 向同一个对象,所以会相互影响。这就是传值和传引用(地址)的区别。

 

1 引用参数

 

CLR允许我们通过关键字(out,ref)来定义参数通过引用方式传递(这里引用方式传递叫引用参数,他和引用类型参数是不同的)。这 两个关键字会告诉编译器,产生额外的元数据来表示参数是按照引用方式传递。两个关键字的区别在与out的参数在使用前可以不被初始化 ,并且被调用方法不能直接读取参数的值,它必须在返回前给;而ref要求参数在使用前被初始化,被调用的方法可以任意读取该参数。

out方法:

class ex1
{
    static void Main()
    {
        Int32 x;
        SetVal (x);              //x不需要被初始化
        Console.WriteLine(x);   //显示“10”

    } 
    static void SetVal(out Int32 v) 
    {
        v =10;                 //此方法必须初始化v 
    }
}

ref方法:

class ex2
{
    static void Main()
    {
        Int32 x = 5;
        AddVal(ref x);           //x需要被初始化
        Console.WriteLine (x);   //显示 “15”

    }
    static void AddVal(ref Int32 v) 
    {
        v+=10;                 //可以直接初始化v 
    }
}

通过上面可以看到,两个关键字的功能基本是一样的,不同的是编译器会根据他们选择不同的机制来确保我们代码的正确运行。CLR还 允许我们使用ref和out来重载方法:

static void add(Point p){.....}

static void add(out Point p){.....}
static void add(ref Point p){.....} //但这2个关键的不能同时存在

在值类型上使用这2个参数可能没太大区别,所以只有在引用类型上使用,才会比较有意义。可以通过ref参数来要求参数在使用前必须 被初始化或进行一些必要的操作;而使用out可以让参数在方法退出前被赋值,这样可以用在参数被返回后使用时是正常的。有一点要注意 的时,在使用关键字的时候,实参数类型必须和形参类型完全一样,而不仅仅是兼容类型。这也是为了保证类型的安全。

class cc
{
    public Int32 val;
}

class ex
{
    static void Main()
    {
        cc c ;
        change(out c);        //error CS1503: 参数“1” : 无法从“out cc”转换为“out object”
        Console.WriteLine(c.val);
        
    } 

    static void change(out Object o)
    {
        o = new String('x',100);
    }

如上,否则可以通过把cc类型的c传给change(out Object o)方法,而在方法类 o = new string(‘x’,100)这个时候o的类型就被改变 了。所以通过完全匹配类型避免的这个安全漏洞。他会产生一个编译时错误。

最后要说到的就是引用类型使用ref参数。一般情况引用类型,参数传递时传递的是引用,形参和实参是指向同一个对象的。但是有个特殊情况,如果我在定义的方法中,给形参分配了新的内存空间,那么就无法返回这个新的对象了。看下面的例子:

class PassingRefByVal 
{
    static void Change(int[] pArray)
    {
        pArray[0] = 888;  // This change affects the original element.
        pArray = new int[5] {-3, -1, -2, -3, -4};   // This change is local.
        System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
    }

    static void Main() 
    {
        int[] arr = {1, 4, 5};
        System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr [0]);

        Change(arr);
        System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr [0]);
    }
}

输出:

Inside Main, before calling the method, the first element is: 1

Inside the method, the first element is: -3

Inside Main, after calling the method, the first element is: 888

在上个示例中,数组 arr 为引用类型,在未使用 ref 参数的情况下传递给方法。在此情况下,将向方法传递指向 arr 的引用的一个副本。输出显示方法有可能更改数组元素的内容,在这种情况下,从 1 改为 888。但是,在 Change 方法内使用 new 运算符来分配新的内存部分,将使变量 pArray 引用新的数组。因此,这之后的任何更改都不会影响原始数组 arr(它是在 Main 内创建的)。实际上,本示例中创建了两个数组,一个在 Main 内,一个在 Change 方法内。

在看看使用ref参数来返回这个新的数组

class PassingRefByRef 
{
    static void Change(ref int[] pArray)
    {
        // Both of the following changes will affect the original variables:
        pArray[0] = 888;
        pArray = new int[5] {-3, -1, -2, -3, -4};
        System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
    }
        
    static void Main() 
    {
        int[] arr = {1, 4, 5};
        System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);

        Change(ref arr);
        System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);
    }
}

输出

Inside Main, before calling the method, the first element is: 1

Inside the method, the first element is: -3

Inside Main, after calling the method, the first element is: -3

方法内发生的所有更改都影响 Main 中的原始数组。实际上,使用 new 运算符对原始数组进行了重新分配,这个时候形参和实参还是指向同一个对象,而不是想第一个例子那样,2个数组相互不影响了。因此,调用 Change 方法后,对 arr 的任何引用都将指向 Change 方法中创建的五个元素的数组。

对于String类型使用ref参数是有用的,因为String是不可变的,修改一个String对象后,都会重新分配一个新的内存空间。所以要实现交换2个字符串,就必须使用ref。具体例子会在后面的String章节中介绍。

 

2 可变数目参数

有时候我门并不能确定有多少个参数,这个时候就可以使用可变参数:

static Int32 Add(pararms Int32[] values) 
{
  Int32 sum = 0;
  for (Int32 x = 0; x<values.Length;x++)
  {
     sum += values[x];
  } 
  return sum;
}

这个里方法参数中有一个params关键字。这个关键字告诉编译器在指定参数上使用System.ParamsArrayAttribute定制特性。在编译时 如果有这个特性,就会构造一个数组和用指定的元素填充数组,然后调用这个方法。

需要注意的是,参数中只有最后一个参数才能使用这个特性。而且这个参数必须为一个一维数组,类型任意。可以传递null或者0长数 组给此参数。当要编写接受多个任意类型参数的方法时,只需要把参数类型改为Object就可以了。

 

3 虚方法

 

在面向对象语言中,最重要的就是虚方法。正式因为有了他,才实现了程序的多态。通过对基类中虚方法的重写或隐藏,可以实现自己类型的方法。具体这个以后在多态中在慢慢研究,这里就不废话了。下面是一张类型的内存分配模型:

 

 

六  属性

 

 

当在类型中把一个字段定义为私有类型时,他无法在其他类型中直接访问,这就确保了数据封装的安全性。而有时我们还希望在其他类型中访问和修改这些字段。但因为是私有的,我们无法实现;如果定义为公有,又破坏了封装的安全性;为了解决在其他类型中访问类型的私有字段,我么可以使用访问器方法来实现。

 

1 无参属性

 

反问器方法是类型中的一个方法,他是公有的,可以在其他类型中被访问。所以我们可以把对私有字段的操作放在这个方法中,其他类型通过调用这个方法来访问私有字段。在方法器中我们还可以进行一些检查和额外的操作

class ex
{   
    int n;
    public ex()
    {
        n = 1;
    }
    
    public void SetVal (int n)
    {
        if(n<0)
            Console.WriteLine("n要大于等于0");
        else
            this.n = n;
    }
    
    public int GetVal()
    {
        return this.n;
    }

}

class examlpe
{
    static void Main()
    {
        ex o = new ex();
        Console.WriteLine(o.GetVal());
        o.SetVal(10);
        Console.WriteLine(o.GetVal());
        o.SetVal(-10);
        Console.WriteLine(o.GetVal());

    }
}

从上面的程序可以看出来,通过GetVal和SetVal两个访问器方法实现了对私有字段的访问和修改,也保证了数据的安全,而且还在方法中增加了额外的操作和检查。但这种方法存在的问题是:

  1. 要写一些额外的代码来访问。
  2. 只能通过方法名来访问字段,没有直接访问直观。

CLR提供的属性,来缓解了第一个问题和完全解决了第二个问题。修改后的代码如下。

class ex
{   
    int _n;             //定义的私有字段用'_'避免命名冲突
    public ex()
    {
        _n = 1;
    }
    
    public int n  
    {
        get{ return _n; }  
        set
        {
            if(value<0)
                Console.WriteLine("n需要大于等于0");
            else
                _n = value;
        }
    }
} 

class examlpe
{
    static void Main()
    {
        ex o = new ex();
        Console.WriteLine(o.n);
        o.n = 10;
        Console.WriteLine(o.n);
        o.n =- 10;
        Console.WriteLine(o.n);

    }
}

每个属性都要有一个类型(不能为void)和一个名称,属性不能被重载。我们定义属性时为他指定get和set两个方法,缺少get则为只写,缺少set为只读。当这段代码被编译时,系统会自动产生2个方法,自动在属性前加get_和set_,这个和之前的操作符和转换符的重载比较类似,也有一个specialname 标记。除此之外系统还会在元数据内产生一些定义。

.method public hidebysig specialname instance int32  get_n() cil managed

.method public hidebysig specialname instance void set_n(int32 'value') cil managed

在使用属性时,系统实际上会调用这2个方法,如果使用语言不支持属性,我们可以通过前面访问器的方法来访问,只是没有使用属性看起来方便。对于简单的get和set访问器方法,系统会使用内联的方式处理,所以不会有性能损失。有些比较复杂的计算或操作,我们不应该放在属性的访问器方法中,而应该用方法来完成。

 

2 含参属性

 

前面看到的属性get访问器方法不接受任何参数。如果接受一个或多个参数,在C#中我们把他称为索引器,也就是含参数属性。C#中索引器是类似数组的语法来访问。我们也可以将索引器看做是重载[ ] 操作符的一种方式。

class BitArray
{   
    //保存位的私有字节数组
    private Byte[] byteArray;
    private Int32 numBits;

    //构造函数分配字节数组,所有位置0
    piblic BitArray(Int32 numBits)
    {
        if (numBits<=0)
            throw new ArgumentOutRangeRxception("numBits must be>0");
        this.numBits = numBits;
        byteArray = new Byte[(numBits + 7)/8];
    }

    //索引器
    public Boolean this[Int32 bitPos]
    {
        get
        {
            //检查访问数组位数的有效性
            if((bitPos < 0) || (bitPos >= numBits))
                throw new IndexOutOfRangeException();
            //返回指定索引上的位状态
            return (byteArray[bitPos / 8] & (1 << (bitPos % 8))) != 0;
        }
        set
        {
            if ((bitPos < 0) || (bitPos >= numBits))
                throw new IndexOutOfRangeException();

            if(value)
                byteArray[bitPos / 8] = (Byte)(byteArray[bitPos / 8] | (1 << (bitPos% 8)));
            else
                byteArray[bitPos / 8] = (Byte)(byteArray[bitPos / 8] & ~(1 << (bitPos% 8)));

        }
    }
    
} 

class examlpe
{
    static void Main()
    {
        //分配一个含14个位的BitArray
        BitArray ba = new BitArray(14);

        //调用set访问器
        for(Int32 x = 0;x < 14;x++)
            ba[x] = (x % 2 ==0);
        

        //调用get访问器
        for(Int32 x = 0;x < 14;x++)
            Console.WriteLine("Bit" + x + "is " + (ba[x] ? "On" : "Off"));

    }
}

上面的例子可以看出,索引器接受一个或多个参数,这些参数和返回值可以为任何类型。创建一个Object类型的索引器是十分常见的。对于CLR来说,有参和无参属性对它来说没有什么区别,因为他们都只是一个方法。C#中要求我们使用 this[…..]的格式来创建有参属性,所以我们只能在对象实例上创建索引器,而不能创建静态的索引器,索然CLR支持。

同无参属性一样,有参属性在编译时也回产生两个get_和set_的方法和相关的元数据,但可以注意到,有参属性是不允许我们给索引器命名的,生成方法时系统会自动生成get_Item ,set_Item这样两个方法。SortedList类型就提供了一个名为Item的公有属性,属性就是SortedList的一个索引器。当然我们可以通过在索引器上应用如下特性来实现改变方法的名字:

Public class BitArray
{
   [System.Runtime.ComplierServices.IndexerName("Bit")]
   Public Boolen this[Int32 bitPos]
}
Console.WriteLine(ba[2]);
Console.WriteLine(ba.Bit[2]);

这样我们就可以使用2种方法来访问索引器了。String类型就是一个改变了索引器名字的例子。String把索引器命名为Chars而非Item,该属性允许我们得到一个字符串中的单个字符。对于不使用[ ]操作符号语法来访问含参属性的编程语言来讲,Chars是一个有意义的名称。当编译器遇到读取或设置索引器的代码时,会自动访问get或set方法。但要注意的是在C#,不允许使用索引器的名字来访问,只能通过[ ]来访问。我们还可以给属性的get和set方法设置访问属性,比如private。

0

如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

发表评论

电子邮件地址不会被公开。 必填项已用*标注