由于最近在项目中有一个需求需要用到字符串的拆分,然后将拆分后的元素逐个加到一个listview控件中。代码如下:
namespace InitalTest { class Program { static void Main(string[] args) { string s = "1+2+3+4+5+6"; //问题1:Split方法第一个参数是一个string[]对象,为什么有些方法当参数为string的时候不用new运算符呢? string[] sArrs = s.Split(new string[]{"+"},StringSplitOptions.RemoveEmptyEntries); //问题2:当遍历数组或集合的时候到底选用foreach还是for呢? foreach (string sArr in sArrs) { Console.WriteLine(sArr); } Console.ReadKey(); //程序输出结果: /* 1 2 3 4 5 6 */ } } }
原则上CLR要求所有对象都用new操作符来创建的。比如:Myclass mc=new Myclass();int i=new int();但是有些类型经常使用,所以编译器允许代码以简化的语法来操作它们,比如:int i=new int();可以简写为:int i=0;这种语法不仅增强了代码的可读性,而且生成的IL代码与使用new操作符时生成的IL代码是完全一样的。编译器将这种直接支持的类型称为基元类型。而string就是基元类型,所以创建string对象的时候可以使用简写语法。
这个主要是看for和foreach的区别。
共同点:for和foreach都可以用来遍历数组和集合。
不同点:1,语法。foreach比for更简洁。
2,语义。foreach比for更易懂。
3,性能。foreach比for性能高。
所以,当遍历数组或集合时,最好使用foreach语句。只要对象实现了IEumerable接口。
实例构造器是允许将类型的实例初始化为良好状态的一种特殊方法,它在类的每个新实例创建的时候执行。
代码如下:
namespace ConstructorDemo1 { internal sealed class MyClass//字义类 { DateTime TimeOfInstantiation;//声明字段 public MyClass()//构造函数 { TimeOfInstantiation = DateTime.Now;//初始化字段 } } class Program { static void Main(string[] args) { MyClass mc = new MyClass();//创建对象,同时执行类的实例构造函数 } } }
注意:构造函数的执行顺序为:成员初始化--->基类构造函数调用--->构造函数体执行
类型构造器,也称为静态构造器。它的作用是设置类型(注意不是类型的实例)的初始状态。
代码如下:
internal sealed class MyClass { private static int s_x;//声明静态字段 static MyClass()//类型构造函数 { s_x = 10;//初始化静态字段 } }
注意:类型构造器中的代码只能访问类型的静态字段,并且它的常规用途就是初始化这些字段。
类有两种可见性。
1,public,对所有程序集中的代码可见。
2,internal,仅对定义它的程序集中的代码可见,对其它程序集中的代码不可见。
注:如果没有显式声明类的可见性,C#编译器默认设置为internal。
类的成员共有六种可访问性,这里总结了最常用的三种可访问性修饰符,它们的限制性从上到下,限制性由最大到最小排列。
1,private,只能由定义它的类型中的方法访问。
2,protected,与private相同,除了它允许它的派生类中的方法访问。
3,public,没有访问限制。
注:如果没有显式声明成员的可访问性,C#编译器默认设置为private。另外,任何成员想要被别人访问到,都必须在一个可见的类型内定义。
1,定义类时,除非已经确定将一个类作为基类使用,并允许派生类对其进行特化,否则总是显式地把它指定为sealed类。因为sealed不允许对其派生,这保证了类的安全性。
2,在类的内部,数据字段最好定义为private。这样只能由类内部的方法才能访问,也保证了对象的安全性。
3,在类的内部,方法,属性和事件最好字义为private和非虚。如果要公开类型的某些功能,可以将方法,属性和事件定义为public,否则最好不要公开。
注:OOP的一条格言:当事情变得过于复杂时,就搞更多的类型出来。当一个算法的实现开始变得复杂时,我会定义一些辅助类来封闭独立的功能。
值类型是比引用类型更"轻型"的一种类型,因为它们不作为对象在托管堆中分配,不会被垃圾回收,也不通过指针来引用。但在许多情况下,都需要获取对值类型的一个实例引用。为了将一个值类型转换成一个引用类型,要使用一个名为装箱(boxing)的机制。
装箱即将一个值类型转换成一个引用类型,下面是总结对值类型的一个实例进行装箱时内部发生的事情:
1,在托管堆中分配内存。内存量是值类型的各个字段需要的内存量加上托管堆的所有对象都有的两个额外成员(类型对象指针和同步块索引)需要的内存量。
2,将值类型的字段复制到新分配的堆内存。
3,返回对象的地址(或称指针)。现在,这个地址是对一个对象的引用,值类型现在是一个引用类型。
例如:代码如下:
namespace ValueTypeDemo2
{
//声明一个值类型
struct Point
{
public int x, y;
}
class Program
{
static void Main(string[] args)
{
ArrayList a = new ArrayList();
Point p;//分配一个Point(不在堆中分配)
for (int i = 0; i < 10; i++)
{
p.x = p.y = i;//初始化值类型中的成员
a.Add(p);//对值类型进行装箱,并将引用添加到ArrayList中,因为Add()方法需要的是一个Object对象
}
}
}
}
拆箱实际上就是获取一个指向包含在一个对象中的原始值类型(数据字段)的指针,往往会紧接着拆箱操作后发生一次字段复制的操作,将堆中的字段复制到基于栈的值类型的实例中。
例如:代码如下:
Point p=(Point)a[0];
CLR实际上分两步完成这个复制操作:
1,获取已装箱的Point对象中的各个Point字段的址,这个过程称为拆箱(unboxing)。
2,将这些字段包含的值从堆中复制到基于栈的值类型的实例中。注意,这个过程不属于拆箱的范围。
下面是一个已装箱值类型实例在拆箱时内部发生的事情:
1,如果包含"对已装箱值类型实例的引用"的变量为Null,就抛出一个NullReferenceException异常。
2,如果引用指向的对象不是期待的值类型的一个已装箱实例,就抛出一个InvalidCastException异常。
3,返回一个指向包含在一个对象中的原始值类型的指针。
下面是一个综合实例,代码如下:
namespace ValueTypeDemo3
{
//声明一个值类型
struct Point
{
public int x, y;
}
class Program
{
static void Main(string[] args)
{
Point p;//创建Point的实例,在栈上分配
p.x = p.y = 1;//初始化值类型的成员
object o = p;//对p进行装箱,o引用已装箱的实例
p = (Point)o;//对o进行拆箱,将字段从已装箱的实例中复制到栈变量中
}
}
}
生成的IL代码如下图,可以看到程序发生了一次装箱和一次拆箱:

下面是FCL中值类型和引用类型的区别,用表格总结如下:
|
|
值类型 |
引用类型 |
|
内存分配 |
线程栈 |
托管堆 |
|
垃圾回收 |
不考虑 |
考虑 |
|
表示形式 |
未装箱和已装箱 |
总是已装箱 |
|
是否可以作为基类 |
不能作为基类,不能有虚方法 |
可以作为基类,也能有虚方法 |
|
初始化的值 |
0 |
Null |
|
复制 |
逐字段复制(深拷贝) |
只复制内存地址(浅拷贝) |
|
操作时影响 |
只影响当前对象,不会影响另一个对象 |
这个引用所对应的所有对象 |
namespace ValueTypeDemo1
{
//值类型
struct SomeVal
{
public int x;
}
//引用类型
class SomeRef
{
public int x;
}
class Program
{
static void Main(string[] args)
{
SomeVal v1 = new SomeVal();//在线程栈上分配
SomeRef r1 = new SomeRef();//在托管堆上分配
v1.x = 5;//在栈上修改
r1.x = 5;//提取指针
Console.WriteLine(v1.x);//输出"5"
Console.WriteLine(r1.x);//同样输出"5"
SomeVal v2 = v1;//在栈上分配并深拷贝成员
SomeRef r2 = r1;//只复制引用(指针)
v1.x = 9;//v1.x会更改,v2.x不会更改
r1.x = 8;//r1.x和r2.x都会更改
Console.WriteLine(v1.x);//输出"9"
Console.WriteLine(v2.x);//输出"5"
Console.WriteLine(r1.x);//输出"8"
Console.WriteLine(r2.x);//输出"8"
}
}
}

CLR要求所有的对象都用new操作符来创建,如下代码:
namespace NewobjectDemo1
{
class Employee
{
private string Name;
//构造函数
public Employee(string name)
{
Name = name;
}
}
class Program
{
static void Main(string[] args)
{
Employee e = new Employee("Mcgrady");//使用new操作符创建对象
}
}
}
编译后的IL代码:

注:new操作符在IL中的对应的代码为newobj。
实际上new操作符帮我们做了以下事件:
1,计算类型及其所有基类型所需要的字节数并依此分配内存大小。
2,初始化对象的"类型对象指针"和"同步块索引"成员。
3,调用类型的实例构造器,向其传入在对new 调用中指定的任何实参(上例中就是字符串"Mcgrady")。
方法一:隐式转换和显式(或叫强制)转换
从派生类型向它的基类型转换叫隐式转换,而从基类型向它的某个派生类型转换叫显式转换。如下代码:
namespace NewobjectDemo1
{
//该类型隐式派生自Object
class Employee
{
}
class Program
{
static void Main(string[] args)
{
//隐式转换,因为new返回一个Employee对象,而Object是Employee的基类
Object o = new Employee();
//显式转换,因为Employee派生自Object
Employee e=(Employee)o;
}
}
}
方法二:使用is和as操作符来转型
is检查一个对象是否兼容于指定的类型并返回一个bool值:true或false;as操作符执行实际的转换。如下代码:
namespace NewobjectDemo1
{
//该类型隐式派生自Object
class Employee
{
}
class Program
{
static void Main(string[] args)
{
object o = new object();
//检查对象o是否兼容于类型Employee
if (o is Employee)//如果对象引用为null,is操作符总是返回false,注意is操作符永远不会抛出异常
{
Employee e = o as Employee;//等价于:Employee e=(Employee)o
//在使用变量e之前最好检查引用是否为null
if (e != null)
{
//有if语句中使用e
e.ToString();
}
}
}
}
}