温故知新,CSharp遇见C#两大类型:值类型(内置、特殊)、引用类型(内置、声明)

C#类型中存在两个类别,一个是值类型,一个是引用类型。

image

值类型

值类型的变量包含类型的实例,默认情况下,在分配中,通过将实参传递给方法并返回方法结果来复制变量值。

值类型包括简单类型(字符型、浮点型、整型等)、枚举类型、结构型。

内置值类型

整型数值类型(int...long)

整型数值类型表示整数,所有整型数值类型均为值类型,它们还是简单类型。所有整型数值类型都支持算术、位逻辑、比较、相等运算符。

C#类型/关键字 范围 大小 .NET类型
sbyte -128到127 8位带符号整数 System.SByte
byte 0到255 无符号的8位整数 System.Byte
short -32,768到32,767 有符号16位整数 System.Int16
ushort 0到65,535 无符号16位整数 System.UInt16
int -2,147,483,648到2,147,483,647 带符号的32位整数 System.Int32
uint 0到4,294,967,295 无符号的32位整数 System.UInt32
long -9,223,372,036,854,775,808到9,223,372,036,854,775,807 64位带符号整数 System.Int64
ulong 0到18,446,744,073,709,551,615 无符号64位整数 System.UInt64
nint 取决于(在运行时计算的)平台 带符号的32位或64位整数 System.IntPtr
nuint 取决于(在运行时计算的)平台 无符号的32位或64位整数 System.UIntPtr

浮点数值类型(float..decimal)

浮点数值类型表示实数,所有浮点数值类型均为值类型,它们还是简单类型。所有浮点数值类型都支持算术、位逻辑、比较、相等运算符。

C#类型/关键字 大致范围 精度 大小 .NET类型
float ±1.5x10−45至±3.4x1038 大约6-9位数字 4个字节 System.Single
double ±5.0×10−324到±1.7×10308 大约15-17位数字 8个字节 System.Double
decimal ±1.0x10-28至±7.9228x1028 28-29位 16个字节 System.Decimal

布尔值类型(bool)

bool类型关键字是.NET System.Boolean结构类型的别名,它表示一个布尔值,可为true或false。

若要使用bool类型的值执行逻辑运算,请使用布尔逻辑运算符。bool类型是比较和相等运算符的结果类型。bool表达式可以是if、do、while和for语句中以及条件运算符?:中的控制条件表达式。

bool类型的默认值为false。

字符类型(char)

char类型关键字是.NET System.Char结构类型的别名,它表示Unicode UTF-16字符。

类型 范围 大小 .NET类型
char U+0000到U+FFFF 16位 System.Char

char类型的默认值为\0,即U+0000

char类型支持比较、相等、增量和减量运算符。此外,对于char操作数,算数和逻辑位运算符对相应的字符代码执行操作,并得出int类型的结果。

字符串类型将文本表示为char值的序列。

特殊值类型

结构类型(struct)

结构类型(struct type)是一种可封装数据和相关功能的值类型。一般使用struct关键词定义其结构。

class Program
{
    static void Main(string[] args)
    {
        var tesla = new Tesla(2.0, 30.1);
        Console.WriteLine(tesla);
        Console.ReadLine();
    }
}

public struct Tesla
{
    public double Width { get; }

    public double Height { get; }

    public Tesla(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override string ToString() => $"(Width:{Width}, Height:{Height})";
}

对于struct而言,由于其是值类型,它是在栈(stack)上存储的,可以有效降低内存管理的开销,对于类中包括极小数据的情况,可以考虑使用它。

优势

  • 结构是值类型,在分配内存的时候,速度非常快,因为它们将内联或者保存到栈中,在结构超出作用域被删除时速度也很快
  • 结构体可以把功能相同的数据组织起来,存在一起,用是时候方便,而且在调用函数时,若传递参数较多,传一个结构体相对而言简单一些,很多系统自带的函数必须用结构体。
  • 结构体在使用时可以和枚举一起使用。

成员

  • 结构体成员不能被指定为抽象的、虚拟的、或者保护的对象,因此结构体的成员不能使用如下访问修饰符:abstractvirtualprotected
  • 结构体的函数成员不能声明为abstractvirtual,但是可以使用override关键字,用以覆写它的基类System.ValueType中的方法。

限制

通过New创建结构体对象,必须先初始化所有字段,否则该对象不可用。

public struct Tesla
{
    public double Width { get; }

    public double Height { get; }

    public Tesla(double width, double height)
    {
        Width = width;
        //Height = height;
    }

    public override string ToString() => $"(Width:{Width}, Height:{Height})"; 
}

如果是注释掉构造函数中一个初始化动作,那就会报错。

结构体和类的区别

条件 结构体
数据类型 值类型 引用类型
是否必须使用new运算符实例化
是否可声明无参数的构造函数
数据成员可不可以在声明的同时初始化 声明为const或static可以,数据成员不可以 可以
直接派生自什么类型 System.ValueType System.Object
有无析构函数
可不可以从类派生 不可以 可以
可不可以实现接口 可以 可以
实例化时在栈还是在堆分配内存 堆,栈中保存引用
该类型的变量可不可以被赋值为null 不可以 可以
可不可以定义私有的无参构造函数 不可以 可以
是否总有一个默认的无参构造函数

结构体和类的使用场景

  • 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构好一些。
  • 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低。
  • 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构不支持继承。
  • 大多数情况下,目标类型只是含有一些数据,或者以数据为主,结构体则是最佳选择。

枚举类型(enum)

枚举类型是由基础整型数值类型的一组命名常量定义的值类型。一般使用enum 关键词定义枚举类型并指定枚举成员。

enum Season
{
    Spring,

    Summer,

    Autumn,

    Winter
}

默认情况下,枚举成员的关联常数值为类型int,它们从0开始,并按定义文本顺序递增1,可以显式指定任何其他整数数值类型作为枚举类型的基础类型,还可以显示指定关联的常数值。

enum Season
{
    Spring,

    Summer,

    Autumn = 1000,

    Winter = 2000
}

注意:不能在枚举类型的定义中定义方法。

class Program
{
    static void Main(string[] args)
    {
        Season season = Season.Autumn;
        Console.WriteLine($"{season} value is {(int)season}");
        Console.ReadLine();
    }
}

image

使用ushort也可以作为枚举类型的基础类型。

enum Season : ushort
{
    Spring,

    Summer,

    Autumn = 1000,

    Winter = 2000
}

元组类型(ValueTuple)

元组功能在C# 7.0及更高版本中可用,它提供了简洁的语法,用于将多个数据元素分组成一个轻型数据结构。

class Program
{
    static void Main(string[] args)
    {
        (double, int) t1 = (1.2, 5);
        Console.WriteLine($"Tuple with elements {t1.Item1} and {t1.Item2}");
        Console.ReadLine();
    }
}

输出结果是

Tuple with elements 1.2 and 5

System.ValueTuple类型支持的C#元组不同于System.Tuple类型表示的元组。主要区别如下:

  • System.ValueTuple类型是值类型。System.Tuple类型是引用类型。
  • System.ValueTuple类型是可变的。System.Tuple类型是不可变的。
  • System.ValueTuple类型的数据成员是字段。System.Tuple类型的数据成员是属性。

引用类型

引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。对于引用类型,两种变量可引用同一对象;因此,对一个变量执行的操作会影响另一个变量所引用的对象。对于值类型,每个变量都具有其自己的数据副本,对一个变量执行的操作不会影响另一个变量(in、ref和out参数变量除外)。

内置引用类型

对象类型(object)

object类型是System.Object在.NET中的别名。

在C#的统一类型系统中,所有类型(预定义类型、用户定义类型、引用类型和值类型)都是直接或间接从System.Object继承的。可以将任何类型的值赋给object类型的变量。可以使用文本null将任何object变量赋值给其默认值。

  • 将值类型的变量转换为对象的过程称为装箱
  • 将object类型的变量转换为值类型的过程称为取消装箱

字符串类型(string)

string类型表示零个或多个Unicode字符的序列。string是System.String在.NET中的别名。

尽管string为引用类型,但是定义相等运算符==!=是为了比较string对象(而不是引用)的值。基于值的相等性使得对字符串相等性的测试更为直观。

class Program
{
    static void Main(string[] args)
    {
        string a = "hello";
        string b = "h";
        b += "ello";
        Console.WriteLine(a == b);
        Console.WriteLine(object.ReferenceEquals(a, b));
        Console.ReadLine();
    }
}
True
True

动态类型(dynamic)

dynamic类型表示变量的使用和对其成员的引用绕过编译时类型检查。改为在运行时解析这些操作。dynamic类型简化了对COM API(例如Office Automation API)、动态API(例如IronPython库)和HTML文档对象模型(DOM)的访问。

class Program
{
    static void Main(string[] args)
    {
        dynamic dyn = 1;
        object ojt = 1;
        Console.WriteLine(dyn.GetType());
        Console.WriteLine(ojt.GetType());

        Console.ReadLine();
    }
}

image

在大多数情况下,dynamic类型与object类型的行为类似。具体而言,任何非Null表达式都可以转换为dynamic类型。dynamic类型与object的不同之处在于,编译器不会对包含类型dynamic的表达式的操作进行解析或类型检查。编译器将有关该操作信息打包在一起,之后这些信息会用于在运行时评估操作。在此过程中,dynamic类型的变量会编译为object类型的变量。因此,dynamic类型只在编译时存在,在运行时则不存在

dynamic是从Framework 4.0才有的新特性,它的出现让C#有了弱语言类型的特性。编译器在编译的时候不再对类型进行检查,编译器默认dymanic对象支持你想要的任何特性。

注意,var关键字和dynamic是有本质区别的,var实际上是编译期的一个语法糖,编译期会自动匹配var变量的实际类型,并用实际类型替代该变量的申明。但是dynamic被编译后,实际上是一个object类型,只不过编译器会对dynamic类型做特殊处理,使它可以在编译期不进行任何的类型检查,而是将类型检查放到运行期。

dynamic可以简化类型转换。

dynamic d1 = 7;
dynamic d2 = "a string";
dynamic d3 = System.DateTime.Today;
dynamic d4 = System.Diagnostics.Process.GetProcesses();

int i = d1;
string str = d2;
DateTime dt = d3;
System.Diagnostics.Process[] procs = d4;

dynamic可以简化反射

class DynamicExample
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

class Program
{
    static void Main(string[] args)
    {
        dynamic dynamicExample = new DynamicExample();
        var result = dynamicExample.Add(1,2);
        Console.WriteLine(result);

        Console.ReadLine();
    }
}

声明引用类型

声明类(class)

使用class关键字声明类。

class TestClass
{
    // Methods, properties, fields, events, delegates
    // and nested classes go here.
}

在C#中仅允许单一继承。也就是说,一个类仅能从一个基类继承实现。但是,一个类可实现多个接口

类继承和接口实现的一些示例

继承 示例
class ClassA { }
Single class DerivedClass : BaseClass { }
无,实现两个接口 class ImplClass : IFace1, IFace2 { }
单一,实现一个接口 class ImplDerivedClass : BaseClass, IFace1 { }

类的默认访问修饰符

  • 直接在命名空间中声明的、未嵌套在其它类中的类,可以是公共或内部。默认情况下类为internal

  • 类成员(包括嵌套的类)可以是publicprotected internalprotectedinternalprivateprivate protected。默认情况下成员为private

类可包含的成员申明

  • 构造函数
  • 常量
  • Fields
  • 终结器
  • 方法
  • 属性
  • 索引器
  • 运算符
  • 事件
  • 委托
  • 接口
  • 结构类型
  • 枚举类型
class Child
{
    private int age;
    private string name;

    // Default constructor:
    public Child()
    {
        name = "N/A";
    }

    // Constructor:
    public Child(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    // Printing method:
    public void PrintChild()
    {
        Console.WriteLine("{0}, {1} years old.", name, age);
    }
}

接口(interface)

接口(interface)定义协定。实现该协定的任何class或struct必须提供接口中定义的成员的实现。从C# 8.0开始,接口可为成员定义默认实现。它还可以定义static成员,以便提供常见功能的单个实现。从C# 11开始,接口可以定义static abstractstatic virtual成员来声明实现类型必须提供声明的成员。通常,static virtual方法声明实现必须定义一组重载运算符。

interface ISampleInterface
{
    void SampleMethod();
}

class ImplementationClass : ISampleInterface
{
    // Explicit interface member implementation:
    void ISampleInterface.SampleMethod()
    {
        // Method implementation.
    }

    static void Main()
    {
        // Declare an interface instance.
        ISampleInterface obj = new ImplementationClass();

        // Call the member.
        obj.SampleMethod();
    }
}

接口可以包含如下成员的声明

  • 方法
  • 属性
  • 索引器
  • 事件

实现接口的类可以显式实现该接口的成员。显式实现的成员不能通过类实例访问,而只能通过接口实例访问。此外,只能通过接口实例访问默认接口成员。

interface IPoint
{
    // Property signatures:
    int X { get; set; }

    int Y { get; set; }

    double Distance { get; }
}

class Point : IPoint
{
    // Constructor:
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // Property implementation:
    public int X { get; set; }

    public int Y { get; set; }

    // Property implementation
    public double Distance =>
       Math.Sqrt(X * X + Y * Y);
}

class MainClass
{
    static void PrintPoint(IPoint p)
    {
        Console.WriteLine("x={0}, y={1}", p.X, p.Y);
    }

    static void Main()
    {
        IPoint p = new Point(2, 3);
        Console.Write("My Point: ");
        PrintPoint(p);
    }
}
// Output: My Point: x=2, y=3

委托类型(delegate)

委托类型的声明与方法签名相似。它有一个返回值和任意数目任意类型的参数。

public delegate void MessageDelegate(string message);
public delegate int AnotherDelegate(MyType m, long num);

在.NET中,System.ActionSystem.Func类型为许多常见委托提供泛型定义。可能不需要定义新的自定义委托类型。相反,可以创建提供的泛型类型的实例化。

delegate是一种可用于封装命名方法或匿名方法的引用类型。委托类似于C++中的函数指针;但是,委托是类型安全和可靠的委托是事件的基础。通过将委托与命名方法或匿名方法关联,可以实例化委托

必须使用具有兼容返回类型和输入参数的方法或lambda表达式实例化委托。为了与匿名方法一起使用,委托和与之关联的代码必须一起声明。

使用具有协变类型参数的委托

泛型Func委托中的协变支持的益处。

// Simple hierarchy of classes.  
public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static Employee FindByTitle(String title)  
    {  
        // This is a stub for a method that returns  
        // an employee that has the specified title.  
        return new Employee();  
    }  
  
    static void Test()  
    {  
        // Create an instance of the delegate without using variance.  
        Func<String, Employee> findEmployee = FindByTitle;  
  
        // The delegate expects a method to return Person,  
        // but you can assign it a method that returns Employee.  
        Func<String, Person> findPerson = FindByTitle;  
  
        // You can also assign a delegate
        // that returns a more derived type
        // to a delegate that returns a less derived type.  
        findPerson = findEmployee;  
  
    }  
}

使用具有逆变类型参数的委托

泛型Action委托中的逆变支持的益处

public class Person { }  
public class Employee : Person { }  
class Program  
{  
    static void AddToContacts(Person person)  
    {  
        // This method adds a Person object  
        // to a contact list.  
    }  
  
    static void Test()  
    {  
        // Create an instance of the delegate without using variance.  
        Action<Person> addPersonToContacts = AddToContacts;  
  
        // The Action delegate expects
        // a method that has an Employee parameter,  
        // but you can assign it a method that has a Person parameter  
        // because Employee derives from Person.  
        Action<Employee> addEmployeeToContacts = AddToContacts;  
  
        // You can also assign a delegate
        // that accepts a less derived parameter to a delegate
        // that accepts a more derived parameter.  
        addEmployeeToContacts = addPersonToContacts;  
    }  
}

实践

  • Action可用于没有返回值的方法。
  • Func只能用于有返回值的方法。
class Program
{
    public delegate void BuyBook();

    public static void Book()
    {
        Console.WriteLine("我是卖书的");
    }

    static void Main(string[] args)
    {
        BuyBook buyBook = new BuyBook(Book);
        buyBook();

        Console.ReadLine();
    }
}

这里代表将BuyBook这件事委托给Book函数去执行。

使用Action可以简化,这样可以简化掉delegate的定义,实现同样的效果。

class Program
{
    public static void Book()
    {
        Console.WriteLine("我是卖书的");
    }

    static void Main(string[] args)
    {
        Action buyBook = new Action(Book);
        buyBook();

        Console.ReadLine();
    }
}

如果我要指定买哪本书呢?难道要一本书一本书的定义,其实没必要,可以使用泛型Action来实现。

class Program
{
    public static void Book(string bookName)
    {
        Console.WriteLine($"我是卖书的,我有这本书:{bookName}");
    }

    static void Main(string[] args)
    {
        Action<string> buyBook = new Action<string>(Book);
        buyBook("雷军自传");

        Console.ReadLine();
    }
}

image

接下来,不仅要指定买什么书,还要指定在哪家买。

class Program
{
    public static void Book(string bookName, string place)
    {
        Console.WriteLine($"我是卖书的,我有这本书:{bookName}, 在{place}");
    }

    static void Main(string[] args)
    {
        Action<string,string> buyBook = new Action<string,string>(Book);
        buyBook("雷军自传","深圳");

        Console.ReadLine();
    }
}

我们发现,这等于只是查到了哪家有这个书,那么我要直接运行后拿到这本书怎么办,那就需要返回值,这时候我们就可以切换到Func方法。

class Program
{
    public static string Book(string bookName, string place)
    {
        return $"我是卖书的,我有这本书:{bookName}, 在{place}, 书送来了";
    }

    static void Main(string[] args)
    {
        Func<string,string,string> buyBook = new Func<string,string,string>(Book);
        var book = buyBook("雷军自传","深圳");
        Console.WriteLine(book);
        Console.ReadLine();
    }
}

这里还有点不一样的是,当有两个入参的时候,Func实际定义了三个,其中最后一个是定义的返回值。

Func而言,它是封装了一个不一定具备参数,但是一定返回TResult参数的方法。也就是说,可以没有入参,但是返回是一定有的。

image

妙用Func可以从外部给函数内部传递需要实时获取的值。

class Program
{
    public static string Book(string bookName, string place)
    {
        return $"在{place}有这本书:{bookName}";
    }

    private static void GoToBuyBook(Func<string,string,string> buyBook)
    {
        var user = "我";
        var book = buyBook("雷军自传","深圳");
        Console.WriteLine($"{user}要去买书,{book}");
    }

    static void Main(string[] args)
    {
        Func<string,string,string> buyBook = new Func<string,string,string>(Book);
        GoToBuyBook(buyBook);
        Console.ReadLine();
    }
}

在这个示例中,我们可以把一个Func方法作为入参传入到执行函数内部,而实际执行这个方法却是在函数外部,这也是委托的魔力所在。

除了前面的写法,我们还可以通过Lambda(兰布达)表达式来简化。

private static void GoToBuyBook(Func<string,string,string> buyBook)
{
    var user = "我";
    var book = buyBook("雷军自传","深圳");
    Console.WriteLine($"{user}要去买书,{book}");
}

static void Main(string[] args)
{
    Func<string,string,string> buyBook = (bookName,place) =>
    {
        return $"在{place}有这本书:{bookName}";
    };
    GoToBuyBook(buyBook);
    Console.ReadLine();
}
static void Main(string[] args)
{
    Action<string,string> buyBook = (bookName,place) =>
    {
        Console.WriteLine($"我是卖书的,我有这本书:{bookName}, 在{place}");
    };
    buyBook("雷军自传","深圳");
    Console.ReadLine();
}

Task.Run这里可以传入一个无参数的Action委托。

Task task = Task.Run(()=>{
    Console.WriteLine($"我是卖书的");
});
public static Task Run(Func<Task?> function);
public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);

Task.Factory.StartNew这里可以传入一个无参数的Action委托。

Task taskFactory = Task.Factory.StartNew(() =>
{
    Console.WriteLine($"我是卖书的");
});
public Task StartNew(Action<object?> action, object? state, TaskCreationOptions creationOptions);
public Task StartNew(Action<object?> action, object? state, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler);
public Task StartNew(Action<object?> action, object? state);
public Task StartNew(Action action, TaskCreationOptions creationOptions);
public Task StartNew(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler);
public Task StartNew(Action action, CancellationToken cancellationToken);
public Task StartNew(Action action);

记录(record)

从C# 9开始,可以使用record关键字定义一个record,用来提供用于封装数据的内置功能。C# 10允许record class语法作为同义词来阐明引用类型,并允许record struct使用相同功能定义值类型。通过使用位置参数或标准属性语法,可以创建具有不可变属性的记录类型

public record Person(string FirstName, string LastName);
public record Person
{
    public string FirstName { get; init; } = default!;
    public string LastName { get; init; } = default!;
};
public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
    public double X {  get; init; }
    public double Y {  get; init; }
    public double Z {  get; init; }
}

还可以创建具有可变属性和字段的记录。

public record Person
{
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;
};

记录结构也可以是可变的,包括位置记录结构和没有位置参数的记录结构

public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

记录的主要功能

  • 用于创建具有不可变属性的引用类型的简明语法
  • 内置行为对于以数据为中心的引用类型非常有用:
    • 值相等性
    • 非破坏性变化的简明语法
    • 用于显示的内置格式设置
  • 支持继承层次结构
// 使用record class声明为引用类型记录,class关键字是可选的,当缺省时等价于C#9.0中的record用法
public record Animal;
// 等价于
public record class Animal;

// 使用record struct声明为结构体类型记录
public record struct Animal;
// 也可使用readonly record struct声明为只读结构体类型记录
public readonly record struct Animal;
// 有构造参数,无其它方法属性等
public record Animal(string Name, int Age);
public class Animal : IEquatable<Animal>
{
    public Animal(string Name, int Age)
    {
        this.Name = Name;
        this.Age = Age;
    }

    public string Name { get; init; }
    public int Age { get; init; }

    //其他方法属性
}

记录允许我们自定义构造方法和属性,但是需要遵循

  • 记录在编译时会根据构造参数生成一个默认的构造函数,默认构造函数不能被覆盖,如果有自定义的构造函数,那么需要使用this关键字初始化这个默认的构造函数。
  • 记录中可以自定义属性,自定义属性名可以与构造参数名重名,也就是说自定义属性可以覆盖构造参数生成的属性,此时对应构造参数将不起任何作用,但是我们可以通过属性指向这个构造参数来自定义这样一个属性。

记录是一个语法糖,本质上还是class或者struct,它只编译时生效,运行时并没有记录这个东西。

场景

强制转换和类型转换

由于C#是在编译时静态类型化的,因此变量在声明后就无法再次声明,或无法分配另一种类型的值,除非该类型可以隐式转换为变量的类型。例如,string无法隐式转换为int。因此,在将i声明为int后,无法将字符串“Hello”分配给它,如以下代码所示:

int i;

// error CS0029: Cannot implicitly convert type 'string' to 'int'
i = "Hello";

但有时可能需要将值复制到其他类型的变量或方法参数中。例如,可能需要将一个整数变量传递给参数类型化为double的方法。或者可能需要将类变量分配给接口类型的变量。

这些类型的操作称为类型转换。在C#中,可以执行以下几种类型的转换:

  • 隐式转换:由于这种转换始终会成功且不会导致数据丢失,因此无需使用任何特殊语法。示例包括从较小整数类型到较大整数类型的转换以及从派生类到基类的转换。
// Implicit conversion. A long can
// hold any value an int can hold, and more!
int num = 2147483647;
long bigNum = num;
  • 显式转换(强制转换):必须使用强制转换表达式,才能执行显式转换。在转换中可能丢失信息时或在出于其他原因转换可能不成功时,必须进行强制转换。典型的示例包括从数值到精度较低或范围较小的类型的转换和从基类实例到派生类的转换。
class Test
{
    static void Main()
    {
        double x = 1234.7;
        int a;
        // Cast double to int.
        a = (int)x;
        System.Console.WriteLine(a);
    }
}
// Output: 1234
  • 用户定义的转换:用户定义的转换是使用特殊方法执行,这些方法可定义为在没有基类和派生类关系的自定义类型之间启用显式转换和隐式转换。

  • 使用帮助程序类进行转换:若要在非兼容类型(如整数和System.DateTime对象,或十六进制字符串和字节数组)之间转换,可使用System.BitConverter类、System.Convert类和内置数值类型的Parse方法(如Int32.Parse)。

使用模式匹配以及is和as运算符安全地进行强制转换

var g = new Giraffe();
var a = new Animal();
FeedMammals(g);
FeedMammals(a);
// Output:
// Eating.
// Animal is not a Mammal

SuperNova sn = new SuperNova();
TestForMammals(g);
TestForMammals(sn);

static void FeedMammals(Animal a)
{
    if (a is Mammal m)
    {
        m.Eat();
    }
    else
    {
        // variable 'm' is not in scope here, and can't be used.
        Console.WriteLine($"{a.GetType().Name} is not a Mammal");
    }
}

static void TestForMammals(object o)
{
    // You also can use the as operator and test for null
    // before referencing the variable.
    var m = o as Mammal;
    if (m != null)
    {
        Console.WriteLine(m.ToString());
    }
    else
    {
        Console.WriteLine($"{o.GetType().Name} is not a Mammal");
    }
}
// Output:
// I am an animal.
// SuperNova is not a Mammal

class Animal
{
    public void Eat() { Console.WriteLine("Eating."); }
    public override string ToString()
    {
        return "I am an animal.";
    }
}
class Mammal : Animal { }
class Giraffe : Mammal { }

class SuperNova { }

装箱和取消装箱

装箱是将值类型转换为object类型或由此值类型实现的任何接口类型的过程。常见语言运行时(CLR)对值类型进行装箱时,会将值包装在System.Object实例中并将其存储在托管堆中。取消装箱将从对象中提取值类型。装箱是隐式的;取消装箱是显式的。装箱和取消装箱的概念是类型系统C#统一视图的基础,其中任一类型的值都被视为一个对象。

整型变量i进行了装箱并分配给对象o

int i = 123;
// The following line boxes i.
object o = i;

可以将对象o取消装箱并分配给整型变量i

o = 123;
i = (int)o;  // unboxing

使用装箱

// String.Concat example.
// String.Concat has many versions. Rest the mouse pointer on
// Concat in the following statement to verify that the version
// that is used here takes three object arguments. Both 42 and
// true must be boxed.
Console.WriteLine(String.Concat("Answer", 42, true));

// List example.
// Create a list of objects to hold a heterogeneous collection
// of elements.
List<object> mixedList = new List<object>();

// Add a string element to the list.
mixedList.Add("First Group:");

// Add some integers to the list.
for (int j = 1; j < 5; j++)
{
    // Rest the mouse pointer over j to verify that you are adding
    // an int to a list of objects. Each element j is boxed when
    // you add j to mixedList.
    mixedList.Add(j);
}

// Add another string and more integers.
mixedList.Add("Second Group:");
for (int j = 5; j < 10; j++)
{
    mixedList.Add(j);
}

// Display the elements in the list. Declare the loop variable by
// using var, so that the compiler assigns its type.
foreach (var item in mixedList)
{
    // Rest the mouse pointer over item to verify that the elements
    // of mixedList are objects.
    Console.WriteLine(item);
}

// The following loop sums the squares of the first group of boxed
// integers in mixedList. The list elements are objects, and cannot
// be multiplied or added to the sum until they are unboxed. The
// unboxing must be done explicitly.
var sum = 0;
for (var j = 1; j < 5; j++)
{
    // The following statement causes a compiler error: Operator
    // '*' cannot be applied to operands of type 'object' and
    // 'object'.
    //sum += mixedList[j] * mixedList[j]);

    // After the list elements are unboxed, the computation does
    // not cause a compiler error.
    sum += (int)mixedList[j] * (int)mixedList[j];
}

// The sum displayed is 30, the sum of 1 + 4 + 9 + 16.
Console.WriteLine("Sum: " + sum);

// Output:
// Answer42True
// First Group:
// 1
// 2
// 3
// 4
// Second Group:
// 5
// 6
// 7
// 8
// 9
// Sum: 30

性能

相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。对值类型进行装箱时,必须分配并构造一个新对象。取消装箱所需的强制转换也需要进行大量的计算,只是程度较轻。

装箱示例

class TestBoxing
{
    static void Main()
    {
        int i = 123;

        // Boxing copies the value of i into object o.
        object o = i;

        // Change the value of i.
        i = 456;

        // The change in i doesn't affect the value stored in o.
        System.Console.WriteLine("The value-type value = {0}", i);
        System.Console.WriteLine("The object-type value = {0}", o);
    }
}
/* Output:
    The value-type value = 456
    The object-type value = 123
*/

取消装箱示例

class TestUnboxing
{
    static void Main()
    {
        int i = 123;
        object o = i;  // implicit boxing

        try
        {
            int j = (short)o;  // attempt to unbox

            System.Console.WriteLine("Unboxing OK.");
        }
        catch (System.InvalidCastException e)
        {
            System.Console.WriteLine("{0} Error: Incorrect unboxing.", e.Message);
        }
    }
}

说个具体的例子,

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Number List:{0},{1},{2}",1,2,3);
        Console.WriteLine("Number List:{0},{1},{2}",1.ToString(),2.ToString(),3.ToString());

        Console.ReadLine();
    }
}

第一行的方式因为从Int值类型要转成string引用类型就存在装箱,第二行这种就直接输入string类型,就减少了装箱的情况。

将字节数组转换为int

使用BitConverter类将字节数组转换为int然后又转换回字节数组。例如,在从网络读取字节之后,可能需要将字节转换为内置数据类型。

返回类型 方法
bool ToBoolean(Byte[],Int32)
char ToChar(Byte[],Int32)
double ToDouble(Byte[],Int32)
short ToInt16(Byte[],Int32)
int ToInt32(Byte[],Int32)
long ToInt64(Byte[],Int32)
float ToSingle(Byte[],Int32)
ushort ToUInt16(Byte[],Int32)
uint ToUInt32(Byte[],Int32)
ulong ToUInt64(Byte[],Int32)
byte[] bytes = { 0, 0, 0, 25 };

// If the system architecture is little-endian (that is, little end first),
// reverse the byte array.
if (BitConverter.IsLittleEndian)
    Array.Reverse(bytes);

int i = BitConverter.ToInt32(bytes, 0);
Console.WriteLine("int: {0}", i);
// Output: int: 25
byte[] bytes = BitConverter.GetBytes(201805978);
Console.WriteLine("byte array: " + BitConverter.ToString(bytes));
// Output: byte array: 9A-50-07-0C

字符串转换为数字

你可以调用数值类型(int、long、double等)中找到的Parse或TryParse方法或使用System.Convert类中的方法将string转换为数字。

调用TryParse方法(例如,int.TryParse("11",outnumber))或Parse方法(例如,varnumber=int.Parse("11"))会稍微高效和简单一些。使用Convert方法对于实现IConvertible的常规对象更有用。

对预期字符串会包含的数值类型(如System.Int32类型)使用Parse或TryParse方法。Convert.ToInt32方法在内部使用Parse。Parse方法返回转换后的数字;TryParse方法返回布尔值,该值指示转换是否成功,并以out参数形式返回转换后的数字。如果字符串的格式无效,则Parse会引发异常,但TryParse会返回false。调用Parse方法时,应始终使用异常处理来捕获分析操作失败时的FormatException。

调用Parse或TryParse方法

Parse和TryParse方法会忽略字符串开头和末尾的空格,但所有其他字符都必须是组成合适数值类型(int、long、ulong、float、decimal等)的字符。如果组成数字的字符串中有任何空格,都会导致错误。例如,可以使用decimal.TryParse分析“10”、“10.3”或“10”,但不能使用此方法分析从“10X”、“10”(注意嵌入的空格)、“10.3”(注意嵌入的空格)、“10e1”(float.TryParse在此处适用)等中分析出10。无法成功分析值为null或String.Empty的字符串。在尝试通过调用String.IsNullOrEmpty方法分析字符串之前,可以检查字符串是否为Null或为空。

using System;

public static class StringConversion
{
    public static void Main()
    {
        string input = String.Empty;
        try
        {
            int result = Int32.Parse(input);
            Console.WriteLine(result);
        }
        catch (FormatException)
        {
            Console.WriteLine($"Unable to parse '{input}'");
        }
        // Output: Unable to parse ''

        try
        {
            int numVal = Int32.Parse("-105");
            Console.WriteLine(numVal);
        }
        catch (FormatException e)
        {
            Console.WriteLine(e.Message);
        }
        // Output: -105

        if (Int32.TryParse("-105", out int j))
        {
            Console.WriteLine(j);
        }
        else
        {
            Console.WriteLine("String could not be parsed.");
        }
        // Output: -105

        try
        {
            int m = Int32.Parse("abc");
        }
        catch (FormatException e)
        {
            Console.WriteLine(e.Message);
        }
        // Output: Input string was not in a correct format.

        const string inputString = "abc";
        if (Int32.TryParse(inputString, out int numValue))
        {
            Console.WriteLine(numValue);
        }
        else
        {
            Console.WriteLine($"Int32.TryParse could not parse '{inputString}' to an int.");
        }
        // Output: Int32.TryParse could not parse 'abc' to an int.
    }
}

调用Convert方法

using System;

public class ConvertStringExample1
{
    static void Main(string[] args)
    {
        int numVal = -1;
        bool repeat = true;

        while (repeat)
        {
            Console.Write("Enter a number between −2,147,483,648 and +2,147,483,647 (inclusive): ");

            string input = Console.ReadLine();

            // ToInt32 can throw FormatException or OverflowException.
            try
            {
                numVal = Convert.ToInt32(input);
                if (numVal < Int32.MaxValue)
                {
                    Console.WriteLine("The new value is {0}", ++numVal);
                }
                else
                {
                    Console.WriteLine("numVal cannot be incremented beyond its current value");
                }
           }
            catch (FormatException)
            {
                Console.WriteLine("Input string is not a sequence of digits.");
            }
            catch (OverflowException)
            {
                Console.WriteLine("The number cannot fit in an Int32.");
            }

            Console.Write("Go again? Y/N: ");
            string go = Console.ReadLine();
            if (go.ToUpper() != "Y")
            {
                repeat = false;
            }
        }
    }
}
// Sample Output:
//   Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 473
//   The new value is 474
//   Go again? Y/N: y
//   Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): 2147483647
//   numVal cannot be incremented beyond its current value
//   Go again? Y/N: y
//   Enter a number between -2,147,483,648 and +2,147,483,647 (inclusive): -1000
//   The new value is -999
//   Go again? Y/N: n

在十六进制字符串与数值类型之间转换

此示例输出string中每个字符的十六进制值。首先,将string分析为字符数组。然后,对每个字符调用ToInt32(Char)获取相应的数值。最后,在string中将数字的格式设置为十六进制表示形式。

string input = "Hello World!";
char[] values = input.ToCharArray();
foreach (char letter in values)
{
    // Get the integral value of the character.
    int value = Convert.ToInt32(letter);
    // Convert the integer value to a hexadecimal value in string form.
    Console.WriteLine($"Hexadecimal value of {letter} is {value:X}");
}
/* Output:
    Hexadecimal value of H is 48
    Hexadecimal value of e is 65
    Hexadecimal value of l is 6C
    Hexadecimal value of l is 6C
    Hexadecimal value of o is 6F
    Hexadecimal value of   is 20
    Hexadecimal value of W is 57
    Hexadecimal value of o is 6F
    Hexadecimal value of r is 72
    Hexadecimal value of l is 6C
    Hexadecimal value of d is 64
    Hexadecimal value of ! is 21
 */

此示例分析十六进制值的string并输出对应于每个十六进制值的字符。首先,调用Split(Char[])方法以获取每个十六进制值作为数组中的单个string。然后,调用ToInt32(String,Int32)将十六进制值转换为表示为int的十进制值。示例中演示了2种不同方法,用于获取对应于该字符代码的字符。第1种方法是使用ConvertFromUtf32(Int32),它将对应于整型参数的字符作为string返回。第2种方法是将int显式转换为char。

string hexValues = "48 65 6C 6C 6F 20 57 6F 72 6C 64 21";
string[] hexValuesSplit = hexValues.Split(' ');
foreach (string hex in hexValuesSplit)
{
    // Convert the number expressed in base-16 to an integer.
    int value = Convert.ToInt32(hex, 16);
    // Get the character corresponding to the integral value.
    string stringValue = Char.ConvertFromUtf32(value);
    char charValue = (char)value;
    Console.WriteLine("hexadecimal value = {0}, int value = {1}, char value = {2} or {3}",
                        hex, value, stringValue, charValue);
}
/* Output:
    hexadecimal value = 48, int value = 72, char value = H or H
    hexadecimal value = 65, int value = 101, char value = e or e
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 6F, int value = 111, char value = o or o
    hexadecimal value = 20, int value = 32, char value =   or
    hexadecimal value = 57, int value = 87, char value = W or W
    hexadecimal value = 6F, int value = 111, char value = o or o
    hexadecimal value = 72, int value = 114, char value = r or r
    hexadecimal value = 6C, int value = 108, char value = l or l
    hexadecimal value = 64, int value = 100, char value = d or d
    hexadecimal value = 21, int value = 33, char value = ! or !
*/

使用类型dynamic

C# 4引入了一个新类型dynamic。该类型是一种静态类型,但类型为dynamic的对象会跳过静态类型检查。大多数情况下,该对象就像具有类型object一样。在编译时,将假定类型化为dynamic的元素支持任何操作。因此,不必考虑对象是从COMAPI、从动态语言(例如IronPython)、从HTML文档对象模型(DOM)、从反射还是从程序中的其他位置获取自己的值。但是,如果代码无效,则在运行时会捕获到错误。

例如,如果以下代码中的实例方法exampleMethod1只有一个形参,则编译器会将对该方法的第一个调用ec.exampleMethod1(10,4)识别为无效,因为它包含两个实参。该调用将导致编译器错误。编译器不会检查对该方法的第二个调用dynamic_ec.exampleMethod1(10,4),因为dynamic_ec的类型为dynamic。因此,不会报告编译器错误。但是,该错误不会被无限期疏忽。它将在运行时被捕获,并导致运行时异常。

static void Main(string[] args)
{
    ExampleClass ec = new ExampleClass();
    // The following call to exampleMethod1 causes a compiler error
    // if exampleMethod1 has only one parameter. Uncomment the line
    // to see the error.
    //ec.exampleMethod1(10, 4);

    dynamic dynamic_ec = new ExampleClass();
    // The following line is not identified as an error by the
    // compiler, but it causes a run-time exception.
    dynamic_ec.exampleMethod1(10, 4);

    // The following calls also do not cause compiler errors, whether
    // appropriate methods exist or not.
    dynamic_ec.someMethod("some argument", 7, null);
    dynamic_ec.nonexistentMethod();
}

class ExampleClass
{
    public ExampleClass() { }
    public ExampleClass(int v) { }

    public void exampleMethod1(int i) { }

    public void exampleMethod2(string str) { }
}

在这些示例中,编译器的作用是将有关每个语句的预期作用的信息一起打包到类型化为dynamic的对象或表达式。在运行时,将对存储的信息进行检查,并且任何无效的语句都将导致运行时异常。

大多数动态操作的结果是其本身dynamic。例如,如果将鼠标指针放在以下示例中使用的testSum上,则IntelliSense将显示类型“(局部变量)dynamictestSum”。

dynamic d = 1;
var testSum = d + 3;
// Rest the mouse pointer over testSum in the following statement.
System.Console.WriteLine(testSum);

参考

posted @ 2022-09-04 16:01  TaylorShi  阅读(565)  评论(0编辑  收藏  举报