说到参数传递,必须得弄清值类型和引用类型:
(为了容易表达,我暂且命名存放在堆中的内容为堆中对象,存放在栈上的内容为栈中对象。)值类型存放在栈中,直接访问。如果有:int a=0;int b=a;就产生了两个栈中对象。引用类型需要在堆中显式分配,且不能直接访问,需要在栈中分配一个栈中对象(C++叫指针,C#叫引用)指向其堆中对象。如果:StringBuilder strb = new StringBuilder();StringBuilder strb2 = strb;则在堆中只有一个堆中对象,只是栈中有两个栈中对象指向堆中对象。可以看出:每个变量都是一个栈中对象。不管是值类型还是引用类型,只是值类型的栈中对象就是其内容,而引用类型的栈中对象只是一个指向堆中对象的地址。判断是值类型还是引用类型:
int a1 = 10;
StringBuilder strb1 = new StringBuilder("ABC");int a2 = a1;StringBuilder strb2 = strb1;bool bl1 = object.ReferenceEquals(a1,a2); //false为值类型(因为值类型要装箱)bool bl2 = object.ReferenceEquals(strb1,strb2); //true为引用类型参数传递分值传递和引用传递两种。
通常,在没有显式指出ref和out时都是值传递。值传递:传的是对象的值拷贝。(即函数内参数对象是调用时传递对象的栈中对象的拷贝。)
引用传递:传的是栈中对象的地址。(即函数内参数对象与调用时传递对象完全是同一栈中对象。)现在用例子来说明传值跟传地址的不同:private void button2_Click(object sender, System.EventArgs e)
{ StringBuilder strb1 = new StringBuilder(); StringBuilder strb2 = new StringBuilder(); Test1(strb1); Test2(ref strb2); string str1 = strb1.ToString(); //str1值:"A" string str2 = strb2.ToString(); //str2值:"BC"}void Test1(StringBuilder strb){ //strb和strb1是两个栈中对象,但指向相同的地址,这个操作是改变堆中对象 strb.Append("A"); //这里将strb指向一个新的堆中对象,所以后面的操作与strb1指向的栈中对象无关 strb = new StringBuilder("B"); strb.Append("C");}void Test2(ref StringBuilder strb){ //这里的strb和strb2是同一个栈中对象,所以改变strb的值使其指向另一个对象也等于改变strb2 strb = new StringBuilder("B"); strb.Append("C");}转自
class myclass
{
public int val;
}
class test
{
public void change(ref myclass mc1, myclass mc2)
{
mc1 = new myclass();
mc1.val = 999;
//mc2 = new myclass();
mc2.val = 999;
}
}
class Program
{
static void Main(string[] args)
{
test tc = new test();
myclass m1 = new myclass();
m1.val = 1;
myclass m2 = new myclass();
m2.val = 1;
tc.change(ref m1, m2);
Console.WriteLine("m1.val={0},m2.val={1}", m1.val, m2.val);
}
}
如上代码,如果类test中的注释行被注释掉,则输出为:999,999
如果注释行的代码不被注释而起作用,那么输出为:999,1
原因就是 mc2是通过值传递的引用。
二、ref和out
ref和out关键字都是指明参数是引用类型的,其区别是:
使用ref,则参数必须在使用前初始化,而在方法内被声明为ref的参数可以被修改,也可以不被修改;
使用out,则参数在使用前可以不初始化,但是在方法内部(方法返回之前)必须对声明为out的参数赋值。当参数是引用类型时(比如对象),这种“赋值”不是指某些字段、属性赋值,而是对对象赋值。
三、方法重载
重载的最低要求是参数列表不同(包括参数次序不同),而返回类型、访问修饰符不同都不是重载。
使用ref或out来区分的,视为重载,但是不能同时使用二者:
public void read(string s)
{
Console.WriteLine(s);
}
//Either of following two is ok,but not both
public void read(ref string s)
{
Console.WriteLine(s);
}
public void read(out string s)
{
Console.WriteLine(s);
}
四、重载构造函数
一旦重载了构造函数,编译器将不再提供默认构造函数,可以自己定义一个无参数的构造函数。
五、继承与重载
当联合使用继承和重载时,C#的做法类似于java,而不是C++。C++的重载概念仅限于同一个类中的方法,而C#中派生类和基类的方法仍然被认为是继承。
六、方法隐藏
类似于C++,如果在基类声明一个方法, 派生类中重写该方法并使用new关键字,则隐藏基类方法。(new是默认的,可省略。)
class Employee
{
public void Calpay()
{
Console.WriteLine("Employee.Calpay");
}
public virtual void Work()
{ Console.WriteLine("Employee.Work"); }public virtual void Funs()
{ Console.WriteLine("Employee.Funs"); } }
class SalariedEmployee:Employee
{
public new void Callpay()//new 隐藏父类方法
{ Console.WriteLine("SalariedEmployee.Calpay"); } public new void Work()//new 隐藏父类方法 { Console.WriteLine("SalariedEmployee.Work"); } public override void Funs()//override 重写父类方法 { Console.WriteLine("SalariedEmployee.Funs"); } }
class Program
{
static void Main(string[] args)
{
Employee e = new Employee();
e.Callpay(); e.Work(); e.Funs(); SalariedEmployee se = new SalariedEmployee(); se.Callpay(); se.Work(); se.Funs(); Employee em = new SalariedEmployee();//子类实例化父类,可以调用父类被隐藏的方法 em.Callpay(); em.Work(); em.Funs(); Console.ReadLine(); }
}
输出为:
七、多态、覆盖
与C++类似, 基类方法声明时间关键字virtual,派生类想要覆盖则在声明时加关键字override。
覆盖的、重定义的方法的方分级别必须与它重定义的虚函数级别一致(不低于),虚函数不能声明为private。
new和virtual可以同时使用,不过此时是为这个虚函数建立了一个新的级别。
如果从构造函数调用虚函数,C#类似于java,而不是C++,调用最早派生的重定义的方法。(此时派生类的对象有可能还没有完全构造好。)
八、静态方法
与C++类似,使用static关键字。
静态方法可以访问类中所有静态成员,但不能访问普通实例成员。反过来,非静态方法都可以访问静态成员和非静态成员。
构造函数中不能通过this访问静态成员。
C#的静态方法只能通过 “类名.方法” 的形式访问。
九、静态构造函数
可以声明静态构造函数:不能有参数,不能有访问修饰符,只能访问静态成员。将先调用静态构造函数,再调用非静态。不构造对象,直接调用静态方法或成员也会触发调用静态构造函数。
出处