当前位置:   article > 正文

C# 类型系统

C# 类型系统

1. 隐式类型

c#允许使用 var 声明变量,编译期会通过初始化语句右侧的表达式推断出变量的类型。

// i is compiled as an int
var i = 5;

// s is compiled as a string
var s = "Hello";

// a is compiled as int[]
var a = new[] { 0, 1, 2 };

// expr is compiled as IEnumerable<Customer>
// or perhaps IQueryable<Customer>
var expr =
    from c in customers
    where c.City == "London"
    select c;

// anon is compiled as an anonymous type
var anon = new { Name = "Terry", Age = 34 };

// list is compiled as List<int>
var list = new List<int>();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

使用var来声明变量,我们可以把注意力放在更为重要的使用部分,只需要准确的变量名,既提供正确的语义,剩下的编译期会去操心变量的类型与后面的使用方法是不是匹配。

甚至有的时候,开发者可能会定义错误或者不合适的数据类型,反而不如var方式声明隐式类型。 如 C# :IQueryable & IEnumerable 中:

var q = from c in dbContext.Customers Where c.City == "London" select c;
var finalAnswer = from c in q order by c.Name select c;
  • 1
  • 2

上面的 q 编译期会通过表达式,将 q 退段位 IQueryable 类型,从而将 Where 表达式与第二行的 order by 进行表达树组合,在一次sql查询操作里完成。

假如开发者明确声明了 q 的类型,但是声明为 IEnumerable,那么在第一行结束,就会把 Where 查询到的所有数据传到本地,之后再进行排序。

IEnumerable<Customer> q = from c in dbContext.Customers Where c.City == "London" select c;
var finalAnswer = from c in q order by c.Name select c;
  • 1
  • 2

隐式类型的数据转换

在使用隐式类型的时候,需要注意的是类型转换的问题。

有些转换是宽化转化(widening conversion),有些则是窄化转换(narrowing conversion),窄化转换会导致精度下降,例如从 long 到 int 的转换。

    public class VarTypeConvert
    {
        public static void Run()
        {
            IMagicNumberGenerator<double> doubleMNGenerator = new DoubleMNGenerator();
            IMagicNumberGenerator<float> floatMNGenerator = new FloatMNGenerator();
            IMagicNumberGenerator<int> intMNGenerator = new IntMNGenerator();

            var d = doubleMNGenerator.GenerateMagicNumber();
            var dtotal = 100 * d / 6;
            Console.WriteLine($"Double magic number: {d}, Total: {dtotal}");
            
            var f = floatMNGenerator.GenerateMagicNumber();
            var ftotal = 100 * f / 6;
            Console.WriteLine($"Float magic number: {f}, Total: {ftotal}");

            var i = intMNGenerator.GenerateMagicNumber();
            var itotal = 100 * i / 6;
            Console.WriteLine($"Int magic number: {i}, Total: {itotal}");
        }
    }

-----------------Output--------------------------
Double magic number: 1, Total: 16.666666666666668
Float magic number: 1, Total: 16.666666
Int magic number: 1, Total: 16
  • 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

都是计算 100 * x / 6,但是由于 x 的类型不同,隐式类型的公式结果大不相同。由于代码采用了隐式类型的局部变量,因此编译期自己推断变量的类型,即根据赋值符号右边的部分做出最佳选择。

即使我们将公式结果定义为 double,由于 MagicNumbe 是int类型,程序仍然会按照整数运算规则进行计算:

var i = intMNGenerator.GenerateMagicNumber();
double dtotal = 100 * i / 6;
Console.WriteLine($"Int magic number: {i}, Total: {dtotal}");
// Output: Int magic number: 1, Total: 16
  • 1
  • 2
  • 3
  • 4

2. 匿名类型

隐式类型的局部变量是为了支持匿名类型机制而加入 c# 语言的。如下匿名类型:

var v = new { Amount = 108, Message = "Hello" };
Console.WriteLine(v.Amount + v.Message);
  • 1
  • 2

我们在使用查询表达式的时候,常会使用匿名类型,从而对每个对象的属性子集处理和返回:

var productQuery =
from prod in products
select new { prod.Color, prod.Price };

foreach (var v in productQuery)
{
    Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

匿名类型为我们提供了一种将只读属性封装到单个对象,又不需要显示先定义类型的便捷方法。类名由编译期生成,每个属性的类型由编译期推断

匿名类型支持采用 with 表达式形式的非破坏性修改,从而使开发者可以创造匿名类型的新实例。

var p1 = new NamedPoint("A", 0, 0);
Console.WriteLine($"{nameof(p1)}: {p1}");  // output: p1: NamedPoint { Name = A, X = 0, Y = 0 }
        
var p2 = p1 with { Name = "B", X = 5 };
Console.WriteLine($"{nameof(p2)}: {p2}");  // output: p2: NamedPoint { Name = B, X = 5, Y = 0 }     
  • 1
  • 2
  • 3
  • 4
  • 5

值得注意的是,匿名类的 EqualsGetHashCode 方法是根据属性的 EqualsGetHashCode 定义的,因此仅当同一匿名类型两个实例所有属性都相等,这两个实例相等。

3. 编译时类型 和 运行时类型

编译时类型即直接声明的类型,或推断类型(c# 隐式类型)。

需要注意的是隐式类型的变量,编译器推断出来的类型不一定是我们期望的类型,比如:

// 推断类型为字符串
var message1 = "This is a string of characters";

// 期望类型为 字符的枚举数组
IEnumerable<char> message2 = "This is a string of characters";
  • 1
  • 2
  • 3
  • 4
  • 5

运行时类型则是在代码运行阶段实际使用的类型。编译时类型确定编译期执行的所有操作,包括方法调用解析、重载决策以及可用的隐式显示强制转换。运行时类型确定在运行时解析度所有操作,包括调度虚拟方法、计算 is 和 switch 表达式以及其他类型测试API。

object o = Factory.GetObject();
MyType t = o as MyType;
// as 在运行时判断是否是 MyType,如果是则进行类型转换,否则返回 null
if (t != null) ...
else ...
  • 1
  • 2
  • 3
  • 4
  • 5

再来看一个编译时类型和运行时类型在类型转换时的例子:

namespace LearnCSharp.Type
{
    public class MyType
    {
        public int data { get; set; }
    }

    public class SecondType
    {
        private MyType _value;

        public SecondType()
        {
            _value = new MyType()
            {
                data = 10
            };
        }

        // 定义类型转换从 SecondType -> MyType
        public static implicit operator MyType(SecondType st)
        {
            return st._value;
        }
    }

    public class LTypeFactory
    {
        public static SecondType GetObject()
        {
            return new SecondType();
        }
    }

    // 测试类型转换
    public class Program
    {
        public static void Main(string[] args)
        {
            SecondType secondType = new SecondType();
            // 根据SecondType 中的声明,可以成功转换类型
            MyType myType = secondType;
           Console.WriteLine($"Convert SecondType to MyType success: Data = {myType.data}");

            object o = LTypeFactory.GetObject();
            MyType t = o as MyType; // 由于运行时类型并不是 MyType,as运算符只会判断运行期类型,不会执行强制类型转换
            if (t != null)
            {
                Console.WriteLine($"Convert object to MyType success: Data = {t.data}");
            }
            else 
            {
                Console.WriteLine("Convert object to MyType failed");
            }

            try
            {
                MyType t1;
                t1 = (MyType) o;
                Console.WriteLine($"Convert object to MyType success: Data = {t1.data}");           
            }
            catch (Exception ex) // 强制类型转换以对象的编译期类型为依据,所以会认为 o 是 object,而非 SecondType,从而强制转换失败
            {
                Console.WriteLine($"Convert object to MyType failed: {ex.Message}");
            }

            VarTypeConvert.Run();
        }
    }
}

--------------------Output----------------------------------
Convert SecondType to MyType success: Data = 10
Convert object to MyType failed
Exception thrown: 'System.InvalidCastException' in LearnCSharp.dll
Convert object to MyType failed: Unable to cast object of type 'LearnCSharpType.SecondType' to type 'LearnCSharp.Type.MyType'.

  • 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

4. 装箱和取消装箱操作

装箱是将值类型转换为object类型,或由此值类型实现的任何接口类型的过程,新创建的引用对象相当于一个箱子,分配在堆上面,其中含有原值的一份拷贝

取消装箱则是从对象中提取值类型。会把已经装箱的那个值拷贝一份出来。

相对于简单的赋值而言,装箱和取消装箱过程需要大量的计算,装箱时,需要分配构造新对象。取消装箱所需要的强制转换也需要大量计算。

int i = 123;
object o = i;  // explicit boxing
  • 1
  • 2

image.png

int i = 123;      // a value type
object o = i;     // boxing
int j = (int)o;   // unboxing
  • 1
  • 2
  • 3

image.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/664305
推荐阅读
相关标签
  

闽ICP备14008679号