• C#基础概念二十五问 【二】 [转]


    11.可以使用抽象函数重写基类中的虚函数吗?

    答:

    可以

    需使用 new 修饰符显式声明,表示隐藏了基类中该函数的实现

    或增加 override 修饰符,表示抽象重写了基类中该函数的实现

    示例:

        class BaseClass
        {
            public virtual void F()
            {
                Console.WriteLine("BaseClass.F");
            }
        }
        abstract class  DeriveClass1 : BaseClass
        {
            public abstract new void F();
        }
     
        //感谢watson hua(http://huazhihao.cnblogs.com/)的指点
        //是他提醒了我还可以用这种方法抽象重写基类的虚方法
        abstract class DeriveClass2 : BaseClass
        {
            public abstract override void F();
        }


    12.密封类可以有虚函数吗?

    答:

    可以,基类中的虚函数将隐式的转化为非虚函数,但密封类本身不能再增加新的虚函数

    示例:

        class BaseClass
        {
            public virtual void F()
            {
                Console.WriteLine("BaseClass.F");
            }
        }
        sealed class DeriveClass : BaseClass
        {
            //基类中的虚函数F被隐式的转化为非虚函数
     
            //密封类中不能再声明新的虚函数G
            //public virtual void G()
            //{
            //    Console.WriteLine("DeriveClass.G");
            //}
        }


    13.什么是属性访问器?

    答:

    属性访问器(Property Accessor),包括 get 访问器和 set 访问器分别用于字段的读写操作

    其设计目的主要是为了实现面向对象(OO)中的封装思想。根据该思想,字段最好设为private,一个精巧的类最好不要直接把字段设为公有提供给客户调用端直接访问

    另外要注意属性本身并不一定和字段相联系


    14.abstract 可以和 virtual 一起使用吗?可以和 override 一起使用吗?

    答:

    abstract 修饰符不可以和 static、virtual 修饰符一起使用

    abstract 修饰符可以和 override 一起使用,参见第11点

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example14
    {
        class BaseClass
        {
            public virtual void F()
            {
                Console.WriteLine("BaseClass.F");
            }
        }
        abstract class DeriveClass1 : BaseClass
        {
            //在这里, abstract是可以和override一起使用的
            public abstract override void F();
        }
        class Program
        {
            static void Main(string[] args)
            {
            }
        }
    }


    15.接口可以包含哪些成员?

    答:

    接口可以包含属性、方法、索引指示器和事件,但不能包含常量、域、操作符、构造函数和析构函数,而且也不能包含任何静态成员

    16.类和结构的区别?

    答:
    类:

    类是引用类型在堆上分配,类的实例进行赋值只是复制了引用,都指向同一段实际对象分配的内存

    类有构造和析构函数

    类可以继承和被继承

    结构:

    结构是值类型在栈上分配(虽然栈的访问速度比较堆要快,但栈的资源有限放),结构的赋值将分配产生一个新的对象。

    结构没有构造函数,但可以添加。结构没有析构函数

    结构不可以继承自另一个结构或被继承,但和类一样可以继承自接口

    示例:

    根据以上比较,我们可以得出一些轻量级的对象最好使用结构,但数据量大或有复杂处理逻辑对象最好使用类。

    如:Geoemtry(GIS 里的一个概论,在 OGC 标准里有定义) 最好使用类,而 Geometry 中点的成员最好使用结构

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example16
    {
        interface IPoint
        {
            double X
            {
                get;
                set;
            }
            double Y
            {
                get;
                set;
            }
            double Z
            {
                get;
                set;
            }
        }
        //结构也可以从接口继承
        struct Point: IPoint
        {
            private double x, y, z;
            //结构也可以增加构造函数
            public Point(double X, double Y, double Z)
            {
                this.x = X;
                this.y = Y;
                this.z = Z;
            }
            public double X
            {
                get { return x; }
                set { x = value; }
            }
            public double Y
            {
                get { return x; }
                set { x = value; }
            }
            public double Z
            {
                get { return x; }
                set { x = value; }
            }
        }
        //在此简化了点状Geometry的设计,实际产品中还包含Project(坐标变换)等复杂操作
        class PointGeometry
        {
            private Point value;
            
            public PointGeometry(double X, double Y, double Z)
            {
                value = new Point(X, Y, Z);
            }
            public PointGeometry(Point value)
            {
                //结构的赋值将分配新的内存
                this.value = value;
            }
            public double X
            {
                get { return value.X; }
                set { this.value.X = value; }
            }
            public double Y
            {
                get { return value.Y; }
                set { this.value.Y = value; }
            }
            public double Z
           {
                get { return value.Z; }
                set { this.value.Z = value; }
            }
            public static PointGeometry operator +(PointGeometry Left, PointGeometry Rigth)
            {
                return new PointGeometry(Left.X + Rigth.X, Left.Y + Rigth.Y, Left.Z + Rigth.Z);
            }
            public override string ToString()
            {
                return string.Format("X: {0}, Y: {1}, Z: {2}", value.X, value.Y, value.Z);
            }
        }
        class Program
        {
            static void Main(string[] args)
            {
                Point tmpPoint = new Point(1, 2, 3);
     
                PointGeometry tmpPG1 = new PointGeometry(tmpPoint);
                PointGeometry tmpPG2 = new PointGeometry(tmpPoint);
                tmpPG2.X = 4;
                tmpPG2.Y = 5;
                tmpPG2.Z = 6;
     
                //由于结构是值类型,tmpPG1 和 tmpPG2 的坐标并不一样
                Console.WriteLine(tmpPG1);
                Console.WriteLine(tmpPG2);
     
                //由于类是引用类型,对tmpPG1坐标修改后影响到了tmpPG3
                PointGeometry tmpPG3 = tmpPG1;
                tmpPG1.X = 7;
                tmpPG1.Y = 8;
                tmpPG1.Z = 9;
                Console.WriteLine(tmpPG1);
                Console.WriteLine(tmpPG3);
     
                Console.ReadLine();
            }
        }
    }

    结果:
    X: 1, Y: 2, Z: 3
    X: 4, Y: 5, Z: 6
    X: 7, Y: 8, Z: 9
    X: 7, Y: 8, Z: 9


    17.接口的多继承会带来哪些问题?

    答:

    C# 中的接口与类不同,可以使用多继承,即一个子接口可以有多个父接口。但如果两个父成员具有同名的成员,就产生了二义性(这也正是 C# 中类取消了多继承的原因之一),这时在实现时最好使用显式的声明

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example17
    {
        class Program
        {
            //一个完整的接口声明示例
            interface IExample
            {
                //属性
                string P
                {
                    get;
                    set;
                }
                //方法
                string F(int Value);
                //事件
                event EventHandler E;
                //索引指示器
                string this[int Index]
                {
                    get;
                    set;
                }
            }
            interface IA
            {
                int Count { get; set;}
            }
            interface IB
            {
                int Count();
            }
            //IC接口从IA和IB多重继承
            interface IC : IA, IB
            {
            }
            class C : IC
            {
                private int count = 100;
                //显式声明实现IA接口中的Count属性
                int IA.Count
                {
                    get { return 100; }
                    set { count = value; }
                }
                //显式声明实现IB接口中的Count方法
                int IB.Count()
                {
                    return count * count;
                }
            }
            static void Main(string[] args)
            {
                C tmpObj = new C();
     
                //调用时也要显式转换
                Console.WriteLine("Count property: {0}", ((IA)tmpObj).Count);
                Console.WriteLine("Count function: {0}", ((IB)tmpObj).Count());
     
                Console.ReadLine();
            }
        }
    }

    结果:
    Count property: 100
    Count function: 10000


    18.抽象类和接口的区别?

    答:

    抽象类(abstract class)可以包含功能定义和实现,接口(interface)只能包含功能定义

    抽象类是从一系列相关对象中抽象出来的概念, 因此反映的是事物的内部共性;接口是为了满足外部调用而定义的一个功能约定, 因此反映的是事物的外部特性

    分析对象,提炼内部共性形成抽象类,用以表示对象本质,即“是什么”

    为外部提供调用或功能需要扩充时优先使用接口


    19.别名指示符是什么?

    答:

    通过别名指示符我们可以为某个类型起一个别名

    主要用于解决两个命名空间内有同名类型的冲突或避免使用冗余的命名空间

    别名指示符在所有命名空间最外层定义,作用域为整个单元文件。如果定义在某个命名空间内,那么它只在直接隶属的命名空间内起作用

    示例:

    Class1.cs:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01
    {
        class Class1
        {
            public override string ToString()
            {
                return "com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1";
            }
        }
    }

    Class2.cs:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02
    {
        class Class1
        {
            public override string ToString()
            {
                return "com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02's Class1";
            }
        }
    }

    主单元(Program.cs):

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    //使用别名指示符解决同名类型的冲突
    //在所有命名空间最外层定义,作用域为整个单元文件
    using Lib01Class1 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
    using Lib02Class2 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02.Class1;
     
    namespace Example19
    {
        namespace Test1
        {
            //Test1Class1在Test1命名空间内定义,作用域仅在Test1之内
            using Test1Class1 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
     
            class Class1
            {
                //Lib01Class1和Lib02Class2在这可以正常使用
                Lib01Class1 tmpObj1 = new Lib01Class1();
                Lib02Class2 tmpObj2 = new Lib02Class2();
                //TestClass1在这可以正常使用
                Test1Class1 tmpObj3 = new Test1Class1();
            }
        }
        namespace Test2
        {
            using Test1Class2 = com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01.Class1;
     
            class Program
            {
                static void Main(string[] args)
                {
                    //Lib01Class1和Lib02Class2在这可以正常使用
                    Lib01Class1 tmpObj1 = new Lib01Class1();
                    Lib02Class2 tmpObj2 = new Lib02Class2();
     
                    //注意这里,TestClass1在这不可以正常使用。
                    //因为,在Test2命名空间内不能使用Test1命名空间定义的别名
                    //Test1Class1 tmpObj3 = new Test1Class1();
                    
                    //TestClass2在这可以正常使用
                    Test1Class2 tmpObj3 = new Test1Class2();
     
                    Console.WriteLine(tmpObj1);
                    Console.WriteLine(tmpObj2);
                    Console.WriteLine(tmpObj3);
     
                    Console.ReadLine();
                }
            }
        }
    }

    结果:

    com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1
    com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib02's Class1
    com.nblogs.reonlyrun.CSharp25QExample.Example19.Lib01's Class1

    20.如何手工释放资源?

    答:

     .NET 平台在内存管理方面提供了GC(Garbage Collection),负责自动释放托管资源和内存回收的工作。但在以下两种情况需要我们手工进行资源释放:一、由于它无法对非托管资源进行释放,所以我们必须自己提供方法来释放对象内分配的非托管资源,比如你在对象的实现代码中使用了一个COM对象;二、你的类在运行是会产生大量实例(象 GIS 中的Geometry),必须自己手工释放这些资源以提高程序的运行效率

    最理想的办法是通过实现一个接口显式的提供给客户调用端手工释放对象,System 命名空间内有一个 IDisposable 接口,拿来做这事非常合适,省得我们自己再声明一个接口了

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example20
    {
        class Program
        {
            class Class1 : IDisposable
            {
                //析构函数,编译后变成 protected void Finalize(),GC会在回收对象前会调用调用该方法
                ~Class1()
                {
                    Dispose(false);
                }
     
                //通过实现该接口,客户可以显式地释放对象,而不需要等待GC来释放资源,据说那样会降低效率
                void IDisposable.Dispose()
                {
                    Dispose(true);
                }
     
                //将释放非托管资源设计成一个虚函数,提供在继承类中释放基类的资源的能力
                protected virtual void ReleaseUnmanageResources()
                {
                    //Do something...
                }
     
                //私有函数用以释放非托管资源
                private void Dispose(bool disposing)
                {
                    ReleaseUnmanageResources();
     
                    //为true时表示是客户显式调用了释放函数,需通知GC不要再调用对象的Finalize方法
                    //为false时肯定是GC调用了对象的Finalize方法,所以没有必要再告诉GC你不要调用我的Finalize方法啦
                    if (disposing)
                    {
                        GC.SuppressFinalize(this);
                    }
                } 
            }
            static void Main(string[] args)
            {
                //tmpObj1没有手工释放资源,就等着GC来慢慢的释放它吧
                Class1 tmpObj1 = new Class1();
     
                //tmpObj2调用了Dispose方法,传说比等着GC来释放它效率要调一些
                //个人认为是因为要逐个对象的查看其元数据,以确认是否实现了Dispose方法吧
                //当然最重要的是我们可以自己确定释放的时间以节省内存,优化程序运行效率
                Class1 tmpObj2 = new Class1();
                ((IDisposable)tmpObj2).Dispose();
            }
        }
    }


    21.P/Invoke是什么?

    答:

    在受控代码与非受控代码进行交互时会产生一个事务(transition) ,这通常发生在使用平台调用服务(Platform Invocation Services),即P/Invoke

    如调用系统的 API 或与 COM 对象打交道,通过 System.Runtime.InteropServices 命名空间

    虽然使用 Interop 非常方便,但据估计每次调用事务都要执行 10 到 40 条指令,算起来开销也不少,所以我们要尽量少调用事务

    如果非用不可,建议本着一次调用执行多个动作,而不是多次调用每次只执行少量动作的原则

    22.StringBuilder 和 String 的区别?

    答:

    String 在进行运算时(如赋值、拼接等)会产生一个新的实例,而 StringBuilder 则不会。所以在大量字符串拼接或频繁对某一字符串进行操作时最好使用 StringBuilder,不要使用 String

    另外,对于 String 我们不得不多说几句:

    1.它是引用类型,在堆上分配内存

    2.运算时会产生一个新的实例

    3.String 对象一旦生成不可改变(Immutable)

    3.定义相等运算符(==!=)是为了比较 String 对象(而不是引用)的值

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example22
    {
        class Program
        {
            static void Main(string[] args)
            {
                const int cycle = 10000;
     
                long vTickCount = Environment.TickCount;
                String str = null;
                for (int i = 0; i < cycle; i++)
                    str += i.ToString();
                Console.WriteLine("String: {0} MSEL", Environment.TickCount - vTickCount);
     
                vTickCount = Environment.TickCount;
                //看到这个变量名我就生气,奇怪为什么大家都使它呢? :)
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < cycle; i++)
                    sb.Append(i);
                Console.WriteLine("StringBuilder: {0} MSEL", Environment.TickCount - vTickCount);
     
                string tmpStr1 = "A";
                string tmpStr2 = tmpStr1;
                Console.WriteLine(tmpStr1);
                Console.WriteLine(tmpStr2);
                //注意后面的输出结果,tmpStr1的值改变并未影响到tmpStr2的值
                tmpStr1 = "B";
                Console.WriteLine(tmpStr1);
                Console.WriteLine(tmpStr2);
     
                Console.ReadLine();
            }
        }
    }

    结果:
    String: 375 MSEL
    StringBuilder: 16 MSEL
    A
    A
    B
    A

    23.explicit 和 implicit 的含义?

    答:

    explicit 和 implicit 属于转换运算符,如用这两者可以让我们自定义的类型支持相互交换

    explicti 表示显式转换,如从 A -> B 必须进行强制类型转换(B = (B)A)

    implicit 表示隐式转换,如从 B -> A 只需直接赋值(A = B)

    隐式转换可以让我们的代码看上去更漂亮、更简洁易懂,所以最好多使用 implicit 运算符。不过!如果对象本身在转换时会损失一些信息(如精度),那么我们只能使用 explicit 运算符,以便在编译期就能警告客户调用端

    示例: 

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example23
    {
        class Program
        {
            //本例灵感来源于大话西游经典台词“神仙?妖怪?”--主要是我实在想不出什么好例子了
            class Immortal
            {
                public string name;
                public Immortal(string Name)
                {
                    name = Name;
                }
                public static implicit operator Monster(Immortal value)
                {
                    return new Monster(value.name + ":神仙变妖怪?偷偷下凡即可。。。");
                }
            }
            class Monster
            {
                public string name;
                public Monster(string Name)
                {
                    name = Name;
                }
                public static explicit operator Immortal(Monster value)
                {
                    return new Immortal(value.name + ":妖怪想当神仙?再去修炼五百年!");
                }
            }
            static void Main(string[] args)
            {
                Immortal tmpImmortal = new Immortal("紫霞仙子");
                //隐式转换
                Monster tmpObj1 = tmpImmortal;
                Console.WriteLine(tmpObj1.name);
     
                Monster tmpMonster = new Monster("孙悟空");
                //显式转换
                Immortal tmpObj2 = (Immortal)tmpMonster;
                Console.WriteLine(tmpObj2.name);
     
                Console.ReadLine();
            }
        }
    }

    结果:
    紫霞仙子:神仙变妖怪?偷偷下凡即可。。。
    孙悟空:妖怪想当神仙?再去修炼五百年!

     
    24.params 有什么用?

    答:

    params 关键字在方法成员的参数列表中使用,为该方法提供了参数个数可变的能力

    它在只能出现一次并且不能在其后再有参数定义,之前可以

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace ConsoleApplication1
    {
        class App
        {
            //第一个参数必须是整型,但后面的参数个数是可变的。
            //而且由于定的是object数组,所有的数据类型都可以做为参数传入
            public static void UseParams(int id, params object[] list)
            {
                Console.WriteLine(id);
                for (int i = 0; i < list.Length; i++)
                {
                    Console.WriteLine(list[i]);
                }
            }
     
            static void Main()
            {
                //可变参数部分传入了三个参数,都是字符串类型
                UseParams(1, "a", "b", "c");
                //可变参数部分传入了四个参数,分别为字符串、整数、浮点数和双精度浮点数数组
                UseParams(2, "d", 100, 33.33, new double[] { 1.1, 2.2 });
     
                Console.ReadLine();
            }
        }
    }

    结果:
    1
    a
    b
    c
    2
    d
    100
    33.33
    System.Double[]


    25.什么是反射?

    答:

    反射,Reflection,通过它我们可以在运行时获得各种信息,如程序集、模块、类型、字段、属性、方法和事件

    通过对类型动态实例化后,还可以对其执行操作

    简单来说就是用string可以在runtime为所欲为的东西,实际上就是一个.net framework内建的万能工厂

    一般用于插件式框架程序和设计模式的实现,当然反射是一种手段可以充分发挥其能量来完成你想做的任何事情(前面好象见过一位高人用反射调用一个官方类库中未说明的函数。。。)

    示例:

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    namespace Example25Lib
    {
        public class Class1
        {
            private string name;
            private int age;
     
            //如果显式的声明了无参数构造函数,客户端只需要用程序集的CreateInstance即可实例化该类
            //在此特意不实现,以便在客户调用端体现构造函数的反射实现
            //public Class1()
            //{
            //}
            public Class1(string Name, int Age)
            {
                name = Name;
                age = Age;
            }
            public void ChangeName(string NewName)
            {
                name = NewName;
            }
            public void ChangeAge(int NewAge)
            {
                age = NewAge;
            }
            public override string ToString()
            {
                return string.Format("Name: {0}, Age: {1}", name, age);
            }
        }
    }

    反射实例化对象并调用其方法,属性和事件的反射调用略去

    using System;
    using System.Collections.Generic;
    using System.Text;
     
    //注意添加该反射的命名空间
    using System.Reflection;
     
    namespace Example25
    {
        class Program
        {
            static void Main(string[] args)
            {
                //加载程序集
                Assembly tmpAss = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Example25Lib.dll");
     
                //遍历程序集内所有的类型,并实例化
                Type[] tmpTypes = tmpAss.GetTypes();
                foreach (Type tmpType in tmpTypes)
                {
                    //获取第一个类型的构造函数信息
                    ConstructorInfo[] tmpConsInfos = tmpType.GetConstructors();
                    foreach (ConstructorInfo tmpConsInfo in tmpConsInfos)
                    {
                        //为构造函数生成调用的参数集合
                        ParameterInfo[] tmpParamInfos = tmpConsInfo.GetParameters(); 
                        object[] tmpParams = new object[tmpParamInfos.Length];
                        for (int i = 0; i < tmpParamInfos.Length; i++)
                        {
                            tmpParams[i] = tmpAss.CreateInstance(tmpParamInfos[i].ParameterType.FullName);
                            if (tmpParamInfos[i].ParameterType.FullName == "System.String")
                            {
                                tmpParams[i] = "Clark";
                            }
                        }
     
                        //实例化对象
                        object tmpObj = tmpConsInfo.Invoke(tmpParams);
                        Console.WriteLine(tmpObj);
     
                        //获取所有方法并执行
                        foreach (MethodInfo tmpMethod in tmpType.GetMethods())
                        {
                            //为方法的调用创建参数集合
                            tmpParamInfos = tmpMethod.GetParameters();
                            tmpParams = new object[tmpParamInfos.Length];
                            for (int i = 0; i < tmpParamInfos.Length; i++)
                            {
                                tmpParams[i] = tmpAss.CreateInstance(tmpParamInfos[i].ParameterType.FullName);
                                if (tmpParamInfos[i].ParameterType.FullName == "System.String")
                                {
                                    tmpParams[i] = "Clark Zheng";
                                }
                                if (tmpParamInfos[i].ParameterType.FullName == "System.Int32")
                                {
                                    tmpParams[i] = 27;
                                }
                            }
                            tmpMethod.Invoke(tmpObj, tmpParams);
                        }
     
                        //调用完方法后再次打印对象,比较结果
                        Console.WriteLine(tmpObj);
                    }
                }
     
                Console.ReadLine();
            }
        }
    }

    结果:
    Name: Clark, Age: 0
    Name: Clark Zheng, Age: 27

     

    示例下载:https://files.cnblogs.com/reonlyrun/CSharp25QExample07.rar

    如果你认为还有哪些概念比较重要或容易混淆,可以在回复中提出,我会及时更新这篇随笔 


    一些话:

    To: watson hua,谢谢你帮我改正了第4、11、14和19点的错误,并且让我对索引指示器的理解更全面!

    To: xiao,谢谢你关于“实例化”的详细解释,让这篇随笔中的措词更加精确!

    To: charleschen,谢谢你追问,让第1、第8的提法更恰当!

    To: 装配脑袋,谢谢你提供 internal protected 含义的正确答案!


    本贴子以“现状”提供且没有任何担保,同时也没有授予任何权利
    This posting is provided "AS IS" with no warranties, and confers no rights.
  • 相关阅读:
    inotifywait实时监控文件目录
    centos7支持xming
    ssh目录权限说明
    利用xinetd实现简单web服务器
    python3 使用http.server秒速搭建web服务器
    linux FFMPEG 摄像头采集数据推流
    Linux nginx+rtmp服务器配置实现直播点播
    Nginx中加入nginx-rtmp-module
    ubuntu查看屏幕分辨率
    运用设计原则编写可测试性的代码
  • 原文地址:https://www.cnblogs.com/RobotTech/p/852003.html
Copyright © 2020-2023  润新知