第二节 简单类定义及对象的创建初步
第二节 简单类定义及对象的创建初步
class HelloClass
{
public string userMessage;
//默认构造函数
public HelloClass()
{
Console.WriteLine(“Default ctor called”);
}
//自定义构造函数,在对象创建过程中传入对应的值
public HelloClass(string msg)
{
Console.WriteLine(“Custom ctor called”);
userMessage = msg;
}
public static int Main(string[] args)
{
//使用默认构造函数创建新的对象
HelloClass c1 = new HelloClass();
Console.WriteLine(“Value of userMessage: {0}"n”,c1.userMessage);
//使用自定义构造函数创建新对象
HelloClass c2 = new HelloClass(“Testing”);
Console.WriteLine(“Value of userMessage: {0}"n”,c2.userMessage);
return 0;
}
}
C#明确的区分了类和对象,类是一个用户自定义的类型,对象是这个类在内存中的一个实例。new关键字是创建一个对象的标志方法。
对象必须在创建之后才可以使用,也就意味着无法使用没有new的对象。
new关键字负责给指定对象计算正确的字节数,并从托管堆上获得足够的内存。C#的对象变量事实上是对内存中对象的引用,而不是对象本身。
构造函数的作用
每一个C#类,系统都会自动提供一个空的默认构造函数,默认构造函数会把类的成员数据设置为合适的默认值。构造函数可以自定义。
在C#程序中,我们不需要在显式的去销毁一个对象了,因为.NET的GC会像Java一样去自动释放一个过期的对象。
虽然我们看到在的类的构造函数都是用public来定义的,但是不说明所有的构造函数必须使用public来定义。因为并不是所有情况下,我们的构造函数都是对外开放的。
其实我们上面的例程是有一些设计上的缺陷的。但是它是可以正确运行的。
设置成员的可见性
|
C#访问修饰符 |
作用 |
|
public |
将一个成员标记为即可以从一个对象中访问,又可以从其派生类中访问。 |
|
private |
将一个方法标记为仅可以被这个类的对象访问。 |
|
protected |
将一个方法定义为可以在这个类中使用,也可以在派生类中使用,但是不能通过对象变量来访问。 |
|
internal |
将一个方法定义为可以在同一个程序集内被任何类型所访问,但不能在程序集外被任何类型访问。 |
|
protected internal |
定义了一个对象的访问被限制在当前的程序集,或者当前程序集中从定义它的类所派生的类型中。 |
对于所有成员,包括方法,字段,构造函数等,如果没有标明可见性,那么都默认为private。
通过.运算符(***.***),被声明为public的成员能被对象引用直接访问,private成员不能被对象引用所访问,但是可以在对象内部进行调用。protected成员只有在创建类层次的时候才有实际的用处,internal和protected internal只有在创建.NET代码库(比如托管的*.dll)的时候才有实际用处。
注:只有嵌套类型可以设置为私有的。
设置类型可见性
对于类型(类、接口、结构、枚举、委托)的可见性,只能使用public和internal,而internal是类型的默认可见性。
public class MyClass{}//这个类能够被当前程序集和外部程序集中的其他类型所访问,但是也只有在创建一个代码库时候,我们才会这样去做。
更多的时候我们创建一个类,也仅仅是声明class ***{},这就意味着这个类默认为internal类型的,也就是说仅能在创建它的代码库中可以使用。
类成员变量的默认值
类的成员变量在执行默认构造函数时候,都会被自动设置为一个恰当的默认值。不同类型的变量会设置为不同值。规则如下:
bool值默认设置为false
数值类型设置为0,浮点型的设为0.0
string被设置为null
char设置为’"0’
引用类型被设置为null
根据以上的规则,看下面的程序
class Test
{
public int myInt;
public string myString;
public bool myBool;
public object myObj;
}
对于局部变量来说,与类中的变量就不同了,局部变量系统不会给出默认值的,所以在定义局部变量的时候,我们应当给出局部变量的初始值,否则编译器会报错。
注:局部变量的强制赋值只有在该局部变量为返回值的时候才可以不强制赋值。
定义常量数据:永远不应该再次修改的数据
C#中提供了const关键字,用于定义值永远不变的变量。在C#中const关键字不能用来限制参数或者返回值,只能用于创建局部变量或实例数据。
要赋值给常量的值必须在编译时就已经知道,因此常量成员不能被赋值给对象的引用。
class Program
{
public const string TeamName=”Sixer”;
static void Main(string[] args)
{
ConstData c = new ConstData();
c.BestNBATeam//这里是无法引用的。
ConstData.BestNBATeam;//这样是可以引用的
TeamName//这样是可以引用的
}
}
class ConstData
{
public const string BestNBATeam = "Lakers";
public const double SimplePI = 3.14;
public const bool Truth = true;
}
定义只读字段
与const字段的区别就在于,readonly字段可以在编译时候不知道值是多少,但是一旦建立就不可以再被修改了。
只读字段与常量的另一不同地方:它们的值可以在构造函数的作用域内被指定。当要赋给只读字段的值必须从外部源来读取的时候,这是非常有用的。
class Employee
{
public readonly string SSN;
public Employee(string empSSN)
{
SSN = empSSN;
}
}
静态只读字段:与常量不同只读字段并不隐含为静态的。如果要允许用户从类级别来获得一个只读字段的值,可以使用static关键字。
public static readonly int TeamPlayer= new TeamPlayer(“James”,24,SF);
static关键字
C#类的成员(包括属性和方法)可以用static关键字来定义,这样就可以使该成员可以在类层次上进行访问,而不能从类型的实例调用。
那什么时候使用static来修饰成员呢?一般来说,静态成员都是被(类设计者)认为很平常,不需要创建类型的实体就可以访问的成员。比如我们见到的Console.WriteLine();
注:静态成员只能操作静态成员,当在静态方法中访问动态成员是必然会产生编译错误的。
静态数据
静态数据是一个比较复杂的概念。我们从例程中来理解静态数据。
class SavingAccount
{
public double currBalance;
public SavingAccount(double balance)
{
currBalance = balance;
}
}
我们先看示例中的动态数据currBalance,在每个对象实例化的过程中,都会为currBalance分配内存空间。而当一个类定义了一个静态数据的时候,每个对象引用的都是相同的副本,也就是说每个对象并没有为静态数据分配内存,而是使用同一段内存中的数据。这在我们示例的银行程序中也是非常有意义的。我们将利率设为一个静态字段,那么每个储户对象在计算其余额的时候都将使用相同的利率。
class SavingAccount
{
public double currBalance;
private static double currRate=0.04;
public SavingAccount(double balance)
{
currBalance = balance;
}
//获取和设置利率的静态方法
public static void SetRate(double nowRate)
{
currRate = nowRate;
}
public static double GetRate()
{
return currRate;
}
//设置和获取利率的实例方法
public void SetRateobj(double nowRate)
{
currRate = nowRate;
}
public double GetRateobj()
{
return currRate;
}
public static void Main(string[] args)
{
Console.WriteLine("*************************");
SavingAccount s1 = new SavingAccount(50.0);
SavingAccount s2 = new SavingAccount(100.0);
Console.WriteLine("Current Rate is{0}",s1.GetRateobj());
s1.SetRateobj(0.08);
Console.WriteLine("Current Rate is{0}", s2.GetRateobj());
Console.ReadLine();
}
}
从这个例程我们很容易看出来静态数据的特点了。
静态构造函数
由于使用静态构造函数的机会较少,至少我是没有用过。所以只是说静态构造函数的特点。
1. 一个类只能有一个静态构造函数。
2. 静态构造函数仅执行一次,创建多少实例也是如此。
3. 静态构造函数不带访问修饰符,也不能带任何参数。
4. 当静态构造函数创建类的实例时,或在调用者访问第一个静态成员之前,运行库会调用静态构造函数。
5. 静态构造函数在任何实例级别的构造函数之前执行。
静态类
我们可以使用static来修饰类,这样的类不能被实例化。当然用处很小。
方法中的参数修饰符
|
参数修饰符 |
作用 |
|
无修饰符 |
如果无参数修饰符,则参数按照值传递的方式进行,意味着被调用的方法会收到一个原始数据的副本而不是原始数据。 |
|
out |
输出参数是由被调用的函数赋值的(是引用传递),如果被引用的函数没有给参数赋值,那么就会出现编译错误。 |
|
params |
这个参数修饰符允许将一组可变个数的相同类型的参数作为单独的逻辑参数进行传递。方法只能有一个params修饰符,而且必须是方法的最后一个参数。 |
|
ref |
由调用者赋值的参数,但是按照引用传递。 |
out例程
public static void Add(int x, int y, out int ans)
{
ans = x + y;
}
public static void GetResult()
{
int ans;
Add(20, 30, out ans);
}
当然例程是没有太多实际意义的,对于out修饰符来说,最主要的用途是用于解决多个返回值的问题。由于方法对于简单类型只能返回一个数据,在我们不想创建复杂的结构来返回多个值时候,我们就使用out来实现。
params例程
public static double Average(params int[] value)
{
double sum = 0;
foreach(int i in value)
{
sum += i;
}
return sum / value.Length;
}
调用语法Average(3, 2, 3, 5, 5);
ref
用途:只有希望在方法中对作用域中的数据的值进行改变的时候,才会使用ref进行参数传递(例如:交换,排序)
迭代结构
判断结构
值类型和引用类型 一个相对复杂的问题
一个.NET的数据类型可能是基于值的或基于引用的。
所有数值类型,枚举和结构都是值类型,这些数据类型的内存都是分配在栈上,所以值类型一旦离开了其作用域就会被从内存中删除。结构是一个比较特殊的值类型,它具备了栈分配的数据效率同时又发挥了面向对象的基本优点(封装),并且结构也可以有构造函数,并且可以使用new来创建。
值类型与引用类型的赋值问题?
class Mypoint
{
public int x, y;
}
struct Mypoint
{
public int x, y;
}
class RefApp
{
public void Main(string[] args)
{
Mypoint p1 = new Mypoint();
p1.x = 100;
p1.y = 100;
Mypoint p2 = p1;
Console.WriteLine("p1.x={0}; p1.y={1}",p1.x,p1.y);
Console.WriteLine("p2.x={0}; p2.y={1}", p2.x, p2.y);
p2.x = 900;
Console.WriteLine("p1.x={0} ; p2.x={1}", p1.x, p2.x);
}
}
通过这段代码我们发现很多有趣的现象,这就是值类型与引用类型的区别。
当我们使用结构Mypoint的时候,p1与p2都是值类型,所以在栈中有两个不同的副本,我们对p2的修改不可能影响到p1的值。
而当我们使用类Mypoint的时候,p1与p2就变成了引用类型,引用类型分配在托管堆上,这些对象一直保留在内存中,知道GC将其销毁。默认情况下,一个引用类型的赋值将产生一个对该堆上同一对象的新引用。所以就会产生p2修改时把p1的值也修改了,因为他们修改的是同一个内存空间的对象。
接下来的内容是一些更复杂的关于值类型与引用类型的说明。
1. 包含引用类型的值类型
对于包含引用类型的值类型,在对象赋值的时候,对于引用类型实行浅拷贝,也就是复制的时候复制指向对象的引用。
2. 按值类型传递引用类型的参数
当我们把一个对象当作参数进行传递的时候,默认是作为值类型进行传递的,但是对象是一个引用类型,所以在执行过程中,函数中对该对象的操作会影响这个对象,但是无法将传递的对象创建为一个新的对象。
3. 按引用类型传递引用类型参数
被调用者可以改变对象的状态数据的值和所引用的对象。
值类型与引用类型的比较
|
问题 |
值类型 |
引用类型 |
|
类型分配在哪里? |
分配在栈上 |
分配在拖管堆上 |
|
变量怎样表示? |
值类型变量时局部复制 |
引用类型变量指向被分配的实例所占的内存 |
|
基类是什么? |
必须继承自System.ValueType |
可以继承自除了System.ValueType以外的任何类型,只要那个类型不是密封的。 |
|
这个类型可以作为其他类型的基类吗? |
不能。值类型总是密封的,不能被继承。 |
是的。如果这个类型不是密封的,它可以作为其他类型的基类。 |
|
默认的参数传递行为是什么? |
变量时按值传递的 |
默认是按照引用传递 |
|
这个类型能重写System.Object.Finalize()吗? |
不能,值类型不会放在堆中,所以需要 |
可以间接重写 |
|
可以为这样的类型定义构造函数吗? |
是的,但是默认的构造函数被保留,也就意味着所有自定义构造函数必须带有参数。 |
OK |
|
这个类型的变量什么时候消亡? |
超出作用域后,栈内存会被释放 |
GC进行管理 |
类型变换 装箱(boxing)与拆箱操作
装箱的操作可以将一个值类型显式的转换成对应的引用类型。通过在System.Object中保存来将值类型转换成对应的引用类型。装箱过程中,CLR在堆上分配一个新的对象,并将值类型的值复制到实例中。返回的是一个新分配的对象的引用。
拆箱是将对象引用所保存的值转换成对应的栈上的值类型的过程。拆箱操作会验证接受的数据类型与装箱的类型是否相同。
short s =25;
objct obj=s;//装箱
short s1=(short)obj;//拆箱
事实上很少需要手工进行装箱的操作,考虑下面一个函数的定义:
public static void UseObject(object o){}
在调用这个函数时,当我们传入一个值类型,系统也会自动进行装箱操作。装箱和拆箱的操作不可避免的都会降低性能,这可以通过泛型来解决,这是后话。
枚举类型Enum
C#中最重要的类 System.Object
在.NET环境中所有的类都最终继承自同一个公共的基类System.Object。Object类定义了.NET世界中每一个类型都支持的一组公共的成员集合。当创建一个不显式指定基类的类时,它隐含继承与System.Object。
System.Object源码
namespace System
{
// Summary:
// Supports all classes in the .NET Framework class hierarchy and provides low-level
// services to derived classes. This is the ultimate base class of all classes
// in the .NET Framework; it is the root of the type hierarchy.
[Serializable]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class Object
{
// Summary:
// Initializes a new instance of the System.Object class.
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public Object();
// Summary:
// Determines whether the specified System.Object is equal to the current System.Object.
//
// Parameters:
// obj:
// The System.Object to compare with the current System.Object.
//
// Returns:
// true if the specified System.Object is equal to the current System.Object;
// otherwise, false.
//
// Exceptions:
// System.NullReferenceException:
// The obj parameter is null.
public virtual bool Equals(object obj);
//
// Summary:
// Determines whether the specified System.Object instances are considered equal.
//
// Parameters:
// objA:
// The first System.Object to compare.
//
// objB:
// The second System.Object to compare.
//
// Returns:
// true if objA is the same instance as objB or if both are null references
// or if objA.Equals(objB) returns true; otherwise, false.
//
// Exceptions:
// System.NullReferenceException:
// The obj parameter is null.
public static bool Equals(object objA, object objB);
//
// Summary:
// Serves as a hash function for a particular type.
//
// Returns:
// A hash code for the current System.Object.
public virtual int GetHashCode();
//
// Summary:
// Gets the System.Type of the current instance.
//
// Returns:
// The System.Type instance that represents the exact runtime type of the current
// instance.
public Type GetType();
//
// Summary:
// Creates a shallow copy of the current System.Object.
//
// Returns:
// A shallow copy of the current System.Object.
protected object MemberwiseClone();
//
// Summary:
// Determines whether the specified System.Object instances are the same instance.
//
// Parameters:
// objA:
// The first System.Object to compare.
//
// objB:
// The second System.Object to compare.
//
// Returns:
// true if objA is the same instance as objB or if both are null references;
// otherwise, false.
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static bool ReferenceEquals(object objA, object objB);
//
// Summary:
// Returns a System.String that represents the current System.Object.
//
// Returns:
// A System.String that represents the current System.Object.
public virtual string ToString();
}
}
重写System.Object中的一些默认行为
我们经常会重写ToString()函数,尤其对于某些类中的状态数据进行一定的组织,我们可以在对某个对象进行ToString()的时候得到一个非常有意义的值。比如对于银行账户类,我们可以将ToString()重写为输出账户姓名的相关信息,而不显示其他内容。
在某些情况下我们会重写Equals函数,由于默认情况下Equals函数比较的是两个引用是否是对内存中的同一个对象,有时候我们只是需要比较对象的某些属性数据是否一致,所以我就需要重写这个函数。
当重写一个类的Equals方法时,最佳实践要求我们应当重写GetHashCode()函数,如果不重写,就会有编译器警告。GetHashCode是用于返回一个散列值来唯一标识一个对象。这里我们不再多写了,一般情况下,我们可以对唯一标识对象的某个属性使用String.GetHashCode方法来生成散列值,这个方法非常可靠。
?与??运算符
?可空类型
??在获得的值为空时赋值一个默认值
浙公网安备 33010602011771号