# 类型转换

前面说过,把内存比作一个空间,把变量比作盒子,而不同的类型盒子的大小可能不一样,在程序里可以将不同类型相互转换,按不同的类型操作方式操作这些盒子中的数据,转换过程中可能目标类型大小可能等于源类型大小,比如ushort和char,目标类型大小可能大于或小于源类型。在CSharp中类型转换有两种形式:

  • 隐式转换:编译器自动完成从源类型到目标类型的转换。
  • 显式转换:按照某种类型的额外处理,使其完成到目标类型的转换。

# 隐式转换

隐式转换是编译器自动完成的,比如:

var1 = var2;
shortVal * floatVal

隐式转换的规则是任何源类型如果能其取值范围能完全地包含在目标类型的取值范围内,源类型就可以隐式转换成目标类型。

# 显式转换

如果我们想让short转换成byte,这在short取值在0到255之间时是可以转换的,但是我们需要显式转换,显式转换需要根据类型额外操作。

显式转换有几种方法:

  • 使用强制转换符:

    (destinationType)sourceVar

  • 使用System.Convert中的方法。
  • 针对于string类型和其他类型相互转换:
    • string类型-》其他类型:调用各类型的Parse方法,比如int.Parse()和float.Parse()。
    • 其他类型-》string类型:调用其他类型的ToString()方法。

# 第一种方法

在使用第一种方法时,如果short的值超出了byte的取值范围,我们可以选择让它报错或不报错,即使用checked或unchecked。

byte destinationVar;
short sourceVar=288;
destinationVar = checked((byte)sourceVar);
//destinationVar = unchecked((byte)sourceVar);
Console.WriteLine($"destinationVar is : {destinationVar}");

//使用checked运行结果:
未经处理的异常:  System.OverflowException: 算术运算导致溢出。
1
2
3
4
5
6
7
8

如果在该程序中使用unchecked就不会报错,直接输出结果,当然可以修改默认是报错还是不报错:在Solution Explore窗口选择项目右键-》Properties选项-》Build-》Advanced-》check for arithmetic overflow/underflow。

checkError

# 第二种方法

在使用第二种方法时,使用System.Convert中的方法即可。这种方法与上面不同,不管是否使用checked或unchecked,是否设置项目属性,它都会进行溢出检查:

byte destinationVar;
short sourceVar=288;
destinationVar = System.Convert.ToByte(sourceVar);
Console.WriteLine($"destinationVar is : {destinationVar}");

//运行结果:
未经处理的异常:  System.OverflowException: 值对于无符号的字节太大或太小。
   在 System.Byte.Parse(String s, NumberStyles style, NumberFormatInfo info)
   在 System.Byte.Parse(String s)
1
2
3
4
5
6
7
8
9

# 第三种方法

byte destinationVar;
short sourceVar = 288;
destinationVar = byte.Parse(sourceVar.ToString());
Console.WriteLine($"destinationVar is : {destinationVar}");

//运行结果:
未经处理的异常:  System.OverflowException: 值对于无符号的字节太大或太小。
   在 System.Byte.Parse(String s, NumberStyles style, NumberFormatInfo info)
   在 System.Byte.Parse(String s)
1
2
3
4
5
6
7
8
9

可见第二种和第三种方法虽然调用形式不一样,但最后的调用实质是一样的,都会调用System.Byte.Parse。

# 复杂变量类型

这些复杂的变量类型不是仅声明一个给定类型的变量,而是声明和描述一个用户定义的类型再声明这个新类型的变量。

# 枚举

# 枚举基本操作

枚举类型其值的取值范围是用户提供的值的有限集合。它的操作有:

//定义枚举:
enum <typeName> : <underlyingType>
{
    <Value1>,
    <Value2>,
    <Value3>,
    <Value4>,
    ...
}

//声明变量:
<typeName> <varName>;

//赋值:
<varName> = <typeName>.<value>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

枚举的基本类型可指定也可不指定,要是指定它们可以是byte,sbyte,short,ushort,int,uint,long和ulong。

默认情况下,每个值会根据定义的顺序被自动赋予对应的基本类型,比如value1=0,value2=1,value3=2等等。当然也可以使用=运算符手动指定,可以使用一个值作为另一个枚举的基础值,在使用=手动指定时,未赋值的项会从上一个明确声明的项上的值自动加1,比如下面的value4和value2的值相同:

enum <typename> : <underlyingType>
{
    <value1>=<somevalue>,
    <value2>,
    <value3>=<value1>,
    <value4>,
}
1
2
3
4
5
6
7

注意不能循环赋值,下面是错误的:

enum <typename> : <underlyingType>
{
    <value1>=<value2>,
    <value2>=<value1>,
}

1
2
3
4
5
6

# 枚举的转换

枚举与其他类型的相互转换,以枚举和整型,字符串的转换为例:

enum MyColor:int
{
    red=1,
    green=2,
    blue=3
}
static void Main(string[] args)
{
    //初始化
    MyColor mc1 = MyColor.red;

    //枚举转换成整型
    int mc1Int = (int)mc1;
    Console.WriteLine(mc1Int);

    //枚举转换成字符串
    string mc1Str = mc1.ToString();
    Console.WriteLine(mc1Str);

    //字符串转换成整型
    mc1Str = "blue";
    mc1 = (MyColor)Enum.Parse(typeof(MyColor), mc1Str);
    Console.WriteLine(mc1);

    //整型转换成枚举
    mc1Int = 2;
    mc1 = (MyColor)Enum.Parse(typeof(MyColor),Enum.GetName(typeof(MyColor),mc1Int));
    Console.WriteLine(mc1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

关于枚举和其他类型之间的转换,请看下面的实例,实例想输入一个月份,然后输出这个月份有多少天:

public class MyMonth
{
    string monthInputStr;
    enum monthDay : int
    {
        m1 = 31,= m1,
        m2 = 28,= m2,
        m3 = 31,= m3,
        m4 = 30,= m4,
        m5 = 31,= m5,
        m6 = 30,= m6,
        m7 = 31,= m7,
        m8 = 31,= m8,
        m9 = 30,= m9,
        m10 = 31,= m10,
        m11 = 30,
        十一 = m11,
        m12 = 31,
        十二 = m12
    }
    monthDay monthDayVar;

    public void GetAllMonthDay()
    {
        foreach (string strMonth in Enum.GetNames(typeof(monthDay)))
        {
            Console.WriteLine("Current Month is:" + strMonth);
            GetDay(strMonth);
        }
    }

    public void GetDay(string monthDayStr)
    {
        monthDayVar = (monthDay)Enum.Parse(typeof(monthDay), monthDayStr);
        Console.WriteLine($"the day of month is:{(int)monthDayVar}");
    }

    public void monthOper()
    {
        Console.WriteLine("Input a month:");
        monthInputStr = Console.ReadLine();
        if(Enum.IsDefined(typeof(monthDay), monthInputStr))
        {
            GetDay(monthInputStr);
        }
        else
        {
            int tMonthNum;
            if (int.TryParse(monthInputStr, out tMonthNum))
            {
                if (tMonthNum < 1 || tMonthNum > 12)
                {
                    Console.WriteLine("out of month range");
                }
                else
                {
                    GetDay("m" + monthInputStr);
                }
            }
            else
            {
                Console.WriteLine("out of month range!");
            }
            /*
            try
            {
                int tMonthNum = int.Parse(monthInputStr);
                if (tMonthNum < 1 || tMonthNum > 12)
                {
                    Console.WriteLine("out of month range");
                }
                else
                {
                    GetDay("m" + monthInputStr);
                }
            }
            catch (FormatException e)
            {
                Console.WriteLine("out of month range");
            }
            */
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

# 标志枚举

它是一种特殊的枚举,先来讲下二进制的按位运算。

int a = 1;
int b = 5;
int c = a & b;
int d = a | b;
int e = a ^ b;
1
2
3
4
5

c,d,e的值是1,5,4。它们都是将a和b转换成二进制,然后每一位都进行与,或,异或运算,最后得到结果。

那它和标志枚举有什么关系呢?标志枚举是一种特殊的枚举,它的值不是按原来递增1来作为默认值,而是以2的次幂作为默认值,这样做有什么好处呢?现在假设一个问题,有A和B两个学生选修语文,英语,数学,科学四门课程,那么我们如何选用数据结构呢?我们假设用两个数,numA和numB来表示A和B的选修结果,它们的初始值为0,那如何表示呢?

我们让四个数来代表四门课:

语文=1 英语=2 数学=4 科学=8

为什么要让这四个数来代表四门课程呢?我们把它转换成二进制看下:

语文=0001 英语=0010 数学=0100 科学=1000

学生A如果选修了相应的课程,就让numA与相应学科所代表的值与numA做或运算,假如学生选修了语文和数学,那numA=5。假如我们看学生A是否选修了英语,就让英语的值与numA做与运算,结果为0。假如学生B选修了数学和英语,那么numB=6。现在我们想知道学生A与学生B共同选了哪门课程,就让numA & numB,结果为4,这个值与数学的值做与运算不为0,这就说明了它们共同选修了数学,与其他科目的值做与运算均为0,就说明了他们没有共同选修其他科目。同理,numA | numB代表两个学生一共选修的课程,numA ^ numB代表了学生A和学生B只有一人选修的课程。

如果没用[Flags]声明枚举,那么它在输出时只显示能转换成subject的值,就是他们共同选的课程,如果使用[Flags]声明的话,就会根据值显示代表的所有枚举值,比如他们一共选修的课程,下面来看实例:

[Flags]
enum subject
{
    LANGUAGE=1,
    ENGLISH=2,
    MATH=4,
    SCIENCE=8
}
public static void Main(string[] args)
{
    foreach(var one in Enum.GetValues(typeof(subject)))
    {
        Console.WriteLine((int)one);
    }
    subject numA=0, numB=0;
    numA |= subject.LANGUAGE;
    numA |= subject.MATH;
    numB |= subject.ENGLISH;
    numB |= subject.MATH;
    Console.WriteLine($"学生A:{numA};学生B:{numB}");
    Console.WriteLine($"学生A和学生B共同选修的课程:{numA & numB}");
    Console.WriteLine($"学生A和学生B一共选修的课程:{numA | numB}");
    Console.WriteLine($"学生A和学生B中只有一人选修的课程:{numA ^ numB}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 结构体

结构是由几个不同类型数据组成的一个结构,它们在逻辑上通常作为一个整体。看下它的基本操作:

//定义结构:
struct <typeName>
{
    <accessibility> <type> <name>;
}

//定义结构体变量:
<typeName> <varName>;
1
2
3
4
5
6
7
8

请看它相关的实例:

struct Person
{
    public string name;
    private int age;
    int score;
    public Person(string tName,int tAge,int tScore)
    {
        name = tName;
        age = tAge;
        score = tScore;
    }
    public void GetInfo()
    {
        Console.WriteLine($"name:{name}");
        Console.WriteLine($"age:{age}");
        Console.WriteLine($"score:{score}");
    }
}

static void Main(string[] args)
{
    Person p1 = new Person("apple",20,30);
    p1.GetInfo();
    p1.name = "hi";
    p1.GetInfo();
    //p1.score = 70; //不能访问因为默认访问级别是私有的
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

结构体的知识点:

  • 结构体是一种自定义的数据类型,相当于一个复杂容器,可以存储多种类型,用来封装小型相关变量组。
  • 结构体成员可以包括字段,属性和方法。

# 结构体与类比较

结构体与类的区别:

  • 结构体中,默认构造函数一直存在,不能手动添加默认构造函数,它的构造函数需要有形参;类中添加了自定义构造函数必须手动写出默认构造函数。
  • 结构体不能在声明成员变量时初始化,在结构体的构造函数中,必须为结构体的所有字段赋值。
  • 创建结构体对象时可以不使用new关键字,但是这样的结构体对象字段是没有初始值的。
  • 结构体一种值类型,不可以继承,不可以使用protected,存储在栈上;类是引用类型,可以继承,存储在堆上。
  • 结构体和类的默认访问级别是internal,它们的成员变量默认访问级别都是私有的。但是结构体成员不能用abstract,sealed,protected修饰。

# 数组

数组类型允许我们存储n个某类型的变量,来看下它的基本操作:

//声明数组:
<baseType>[] <name>;

//初始化数组有两种方式:
int[] myIntArray={1,2,3,4};
int[] myIntArray2=new int[5];//给数组所有元素赋予默认值。
//可以将上面两种方式组合起来,但是数组大小和元素个数必须得匹配:
int[] myIntArray = new int[3]{1,2,3};
//注意如果使用变量定义其大小,该变量必须得是一个常量
const int arraySize=3;
int[] myIntArray = new int[arraySize]{1,2,3};

//注意并非一定要在声明时初始化数组,下面代码是对的:
int[] myIntArray;
myIntArray= new int[3];
myIntArray= {1,2,3};?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

下面来看下数组的实例操作:

static void PrintArray(int[] targetArray)
{
    Console.WriteLine("--------------------");
    foreach (var one in targetArray)
    {
        Console.WriteLine(one);
    }
}
static void Main(string[] args)
{
    const int arraySize = 10;
    int[] myIntArray = new int[arraySize];

    for(int i = 0; i < arraySize; i++)
    {
        myIntArray[i] = i;
    }

    //查看数组的长度
    Console.WriteLine("Array Size:"+myIntArray.Length);
    PrintArray(myIntArray);

    //创建目标数组的浅复制
    int[] myIntArray2;
    myIntArray2 = (int[])myIntArray.Clone();
    PrintArray(myIntArray2);

    //从指定的目标数组索引处开始,将当前一维数组的所有元素复制到指定的一维数组中。
    int[] myIntArray3=new int[15];
    myIntArray.CopyTo(myIntArray3,5);
    PrintArray(myIntArray3);

    //创建目标数组的浅复制
    int[] myIntArray4=new int[arraySize];
    Array.Copy(myIntArray,myIntArray4,8);
    PrintArray(myIntArray4);

    //查找某个元素
    Console.WriteLine("--------------------------");
    Console.WriteLine(Array.IndexOf(myIntArray2,5));
    Console.WriteLine(Array.LastIndexOf(myIntArray2,7));

    Array.Reverse(myIntArray2);
    PrintArray(myIntArray2);
    Array.Sort(myIntArray2);
    PrintArray(myIntArray2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 多维数组

看下基本操作:

//声明多维数组
<baseType>[,,,] <name>;
//初始化
<baseType>[,] baseData=new int[3,2];
<baseType>[,] baseData={{1,2},{3,4},{5,6}};
<baseType>[,] baseData=new int[3,2]{{1,2},{3,4},{5,6}};

//来看实例遍历多维数组:
int[,] baseData=new int[3,2]{{1,2},{3,4},{5,6}};
foreach(var one in baseData){
    Console.WriteLine(one);
}
1
2
3
4
5
6
7
8
9
10
11
12

# 数组的数组

上面的多维数组可被称为矩形数组,因为它每一行的元素个数相同,如果一个数组的每行元素个数不同,它被称为锯齿数组(jagged array)。来看下它的基本操作:

//声明数组的数组:
<baseType>[][] jaggedArray;
//比如:
int[][] jaggedArray;

//错误初始化数组的数组的方式:
jaggedArray = new int[3][4];
jaggedArray = {{1,2,3},{1,2},{1}};

//正确地初始化数组:
//第一种方式,先初始化数组的数组,再依次初始化子数组:
jaggedArray = new int[2][];
jaggedArray[0] = new int[3];
jaggedArray[1] = new int[4];
//第二种方式
jaggedArray = new int[2][]{
    new int[]{1,2,3},
    new int[]{1,2}
};

//查看实例来遍历该数组:
int[][] jaggedArray;
jaggedArray = new int[2][]{
    new int[]{1,2,3},
    new int[]{1,2}
};
foreach(int[] oneLine in jaggedArray){
    foreach(int one in oneLine){
        Console.WriteLine(one);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 字符串处理

# 字符串常量特性

字符串池是字符串很重要的特性:

  1. 字符串常量具备字符串池特性,字符串常量在创建前,首先在字符串池中查找是否存在相同的文本,如果存在则直接返回该对象的引用,如果不存在,则开辟内存空间存储新字符串。这样可以极大提高内存利用率。
  2. 字符串具有不可变性。字符串常量一旦进入内存,就不得再次改变。因为如果在原位置改变,字符串大小变化会影响其他对象内存空间,导致内存泄露。当遇到字符串变量引用新值时,会在内存中新建一个字符串,将该字符串地址交由该变量引用。

字符串在存储字符串池中没有的字符串时会开辟新的内存空间:

public static void Main(string[] args)
{
   string a = "a1";
   string b = a;
   Console.WriteLine(object.ReferenceEquals(a, b));
   a = "s2";
   Console.WriteLine(object.ReferenceEquals(a, b));
}
1
2
3
4
5
6
7
8

字符串使用字符串池中的字符串的情况:

  • 利用字面量创建的string对象。
  • 利用string.Intern()创建的string对象。
  • 字面量+字面量拼接创建的string对象。

public static void Main(string[] args)
{
    string str01 = "悟空";
    string str02 = "悟空";
    Console.WriteLine(object.ReferenceEquals(str01, str02));
    string str03 = "悟" + "空";
    Console.WriteLine(object.ReferenceEquals(str01, str03));
    string str04 = string.Intern("悟空");
    Console.WriteLine(object.ReferenceEquals(str01, str04));
}

1
2
3
4
5
6
7
8
9
10
11
12

不使用字符串池中字符串的情况:


public static void Main(string[] args)
{
    string str01 = "悟空";
    string str02 = "悟";
    string str03 = str02 + "空";
    Console.WriteLine(object.ReferenceEquals(str01, str03));

    string str04 = string.Intern("悟");
    string str05 = str04 + "空";
    Console.WriteLine(object.ReferenceEquals(str01, str05));
}

1
2
3
4
5
6
7
8
9
10
11
12
13

字符串池由CLR来维护,其中的所有字符串对象的值都不相同。只有编译阶段的文本字符常量会被自动添加到驻留池中,运行时期动态创建的字符串不会被加入到驻留池中,但是可以使用string.Intern()动态创建的字符串加入到驻留池中。

public static void Main(string[] args)
{

    string str01 = new string(new char[] { '悟', '空' });
    string str02 = new string(new char[] { '悟', '空' });
    Console.WriteLine(object.ReferenceEquals(str01, str02));
    string str03 = string.Intern(new char[] { '悟', '空' }.ToString());
    string str04 = string.Intern(new char[] { '悟', '空' }.ToString());
    Console.WriteLine(object.ReferenceEquals(str03, str04));

}
1
2
3
4
5
6
7
8
9
10
11

# 避免字符串池

从上面可以看到因为字符串池的原因,操作字符串常量时会有大量临时字符串对象,会有性能损耗,为了避免字符串池带来的问题,可以使用StringBulid。


public static void Main(string[] args)
{
    const int strLenth = 1000;
    long startTime = DateTime.Now.Ticks;
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < strLenth; i++)
    {
        builder.Append(i);
    }
    long endTime = DateTime.Now.Ticks;
    Console.WriteLine("使用StringBuilder消耗时间: " + (endTime-startTime).ToString());

    string result=null;
    startTime = DateTime.Now.Ticks;
    for (int i = 0; i < strLenth; i++)
    {
        result += i;
    }
    endTime = DateTime.Now.Ticks;
    Console.WriteLine("不使用StringBuilder消耗时间: " + (endTime - startTime).ToString());

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

大家可以运行下看下对比。

# 字符串的常见操作

static void PrintStrArray(string[] strArray)
{
    foreach(var one in strArray)
    {
        Console.WriteLine(one);
    }
}
public static void Main(string[] args)
{
    //字符串转换成数组
    string str1 = " hello world ";
    char[] chArray = str1.ToArray<char>();
    foreach(var ch in chArray)
    {
        Console.WriteLine(ch);
    }
    char[] chArray2 = str1.ToCharArray();
    foreach (var ch in chArray2)
    {
        Console.WriteLine(ch);
    }
    //在某位置插入字符串
    string str2 = str1.Insert(6, ",");
    Console.WriteLine(str2);
    //将字符串以某些字符分割成一个字符串数组,注意分割后的字符串前后都没有空格,在分割的时候给删除了。
    string[] str3 = str2.Split(' ', ',');
    PrintStrArray(str3);
    //字符串将字母变成大写
    Console.WriteLine(str1.ToUpper());
    //字符串将字母变成小写
    Console.WriteLine(str1.ToLower());
    //查看字符串是否包含某个子串
    Console.WriteLine(str1.Contains("wo"));
    //字符串中某个字符的第一次出现的位置
    Console.WriteLine(str1.IndexOf('l'));
    //字符串中从某个位置开始截取字符串
    Console.WriteLine(str1.Substring(3));
    //将字符串中的某些字符替换成其他字符
    Console.WriteLine(str1.Replace('l', 'L'));
    //将几个字符串拼接在一起
    string[] joinArray = { "123", "abc", "789" };
    Console.WriteLine(string.Join("^:^",joinArray));
    char[] joinChar = { 'A', 'B', 'C' };
    Console.WriteLine(string.Join<char>("<:>",joinChar));
    //去掉字符串左右两端的空格
    Console.WriteLine(str1.Trim());
    Console.WriteLine(str1.TrimStart());
    Console.WriteLine(str1.TrimEnd());
    //给字符串两端添加字符来让字符串等于指定长度
    Console.WriteLine(str1.PadLeft(15));
    Console.WriteLine(str1.PadRight(15,'-'));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# 类型总结

C#中的类型基本分为值类型和引用类型,值类型存储数据本身,引用类型存储目标的内存地址。下面来看下总结:

datatype