从零开始独立游戏开发学习笔记(十七)--Unity学习笔记(六)--微软C#指南(三)

好了,忙的时候结束了。
继续讲述和对象相关的知识。这一章讲使用模式匹配进行类型转换。
1. 如何安全地格式转换(模式匹配)
由于对象具有多态性。一个具有基类类型的变量是可以存放 derived 类型的变量的值的,但这有可能产生 InvallidCastException。C# 提供了使用模式匹配(pattern match)的格式转换(cast),仅当成功的时候会转换。C# 也提供了 is
和 as
关键字来判断一个值是否为某个类型。
1.1 is
运算符
比如说以下代码:
static void FeedMammal(Animal a) { if (a is Mammmal m)
{
m.Eat();
} else
{
Console.WriteLine($"{a.GetType().Name} is not a Mammal");
}
}
重点在于:
is
后面并不只是一个类型,而是声明了一个 Mammal 类型的变量。并不是说只能这么写。单单写a is Mammal
也行,只是这种语法把类型判断和初始化写在一起,也是可行的一种语法。当判断成功的时候,a 的值会被赋予给了 m。m 的作用域仅仅在于 if 里,甚至连 else 里都无法访问。
1.2 as
运算符
请看以下代码:
static void TestForMammals(Object o) { var m = o as Mammal; if (m != null)
{
Console.WriteLine(m.ToString());
} else
{
Console.WriteLine($"{o.GetType().Name} is not a Mammal");
}
}
as
运算符执行一次转换。如果成功则转换成对应类型,不成功则返回 null。顺便一提,上面的
m != null
也可以换成m is not null
。
1.3 switch 做类型匹配
如下所示的语法也是可以的:
static void PatternMatchingSwitch(System.ValueType val){ switch (val)
{ case int number:
Console.WriteLine(number); break; case long number:
Console.WriteLine(number); break; case decimal number:
Console.WriteLine(number); break; case float number:
Console.WriteLine(number); break; case double number:
Console.WriteLine(number); break; case null:
Console.WriteLine("val is a nullable type with the null value"); break; default:
Console.WriteLine("Could not convert " + val.ToString()); break;
}
}
2. 模式匹配的场景
现代开发经常要用到来自各种不同地方的数据源,因此数据类型也都不一致。
于是文章采用了这么一个场景--在一个收费站收费。根据高峰期和车型收费。 难点在于,数据来源可能是多个不同的外部系统。那么首先假设有这么三个系统(3 个 namespace):
namespace ConsumerVehicleRegistration{ public class Car
{ public int Passengers { get; set; }
}
}namespace CommercialRegistration{ public class DeliveryTruck
{ public int GrossWeightClass { get; set; }
}
}namespace LiveryRegistration{ public class Taxi
{ public int Fares { get; set; }
} public class Bus
{ public int Capacity { get; set; } public int Riders { get; set; }
}
}
即,数据可能以不同的 class 形式存在。
2.1 最基础的收费
写一个最基础的收费类:
using System;using CommercialRegistration;using ConsumerVehicleRegistration;using LiveryRegistration;namespace toll_calculator{ public class TollCalculator
{ public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => 2.00m,
Taxi t => 3.50m,
Bus b => 5.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
};
}
}
这里使用了一个 switch expression 的语法(非 switch statement)。语法一看大概也知道是怎么回事。因为整个是一个 switch,因此 => 跟的就是 return 的值。
{ } 则是匹配所有的 非 null 的 object。必须写在后面,否则就被第一个返回了。
null 则是匹配 null。
2.2 根据乘客收费
为了减少流量,让车辆载客数更高,因此希望乘客越少收费越高。
我们可以改写上面的代码:
public class TollCalculator
{ public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car {Passengers: 0} => 2.00m + 0.50m,
Car {Passengers: 1} => 2.0m,
Car {Passengers: 2} => 2.0m - 0.50m,
Car => 2.00m - 1.0m,
Taxi {Fares: 0} => 3.50m + 1.00m,
Taxi {Fares: 1} => 3.50m,
Taxi {Fares: 2} => 3.50m - 0.50m,
Taxi => 3.50m - 1.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus => 5.00m,
DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m,
DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
};
}
when
的用法也是简洁明了。当并等于某一个值,而是一个判断语句的时候用 when。以上的代码有部分比较重复。比如对于 car 和 taxi,每个乘客数量都要写一整行代码。可以被简化为以下代码:
public decimal CalculateToll(object vehicle) =>
vehicle switch
{
Car c => c.Passengers switch
{ 0 => 2.00m + 0.5m, 1 => 2.0m, 2 => 2.0m - 0.5m,
_ => 2.00m - 1.0m
},
Taxi t => t.Fares switch
{ 0 => 3.50m + 1.00m, 1 => 3.50m, 2 => 3.50m - 0.50m,
_ => 3.50m - 1.00m
}, Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
Bus b => 5.00m, DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m,
DeliveryTruck t => 10.00m,
{ } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), null => throw new ArgumentNullException(nameof(vehicle))
};
可以看到根本没有新的语法。而是再写一个 switch expression。
_ 表示匹配其他所有情况。同理也不能写在前面,因为一定会被匹配上。
2.3 根据高峰时间收费
假设有这么一个需求。周末正常收费。工作日的话,早上的入流量和晚上的出流量双倍收费。其他时间 1.5 倍收费。凌晨则减少为 0.75。
如果写成 if 语句,写倒是可以写,但是效果如下:
public decimal PeakTimePremiumIfElse(DateTime timeOfToll, bool inbound){ if ((timeOfToll.DayOfWeek == DayOfWeek.Saturday) ||
(timeOfToll.DayOfWeek == DayOfWeek.Sunday))
{ return 1.0m;
} else
{ int hour = timeOfToll.Hour; if (hour < 6)
{ return 0.75m;
} else if (hour < 10)
{ if (inbound)
{ return 2.0m;
} else
{ return 1.0m;
}
} else if (hour < 16)
{ return 1.5m;
} else if (hour < 20)
{ if (inbound)
{ return 1.0m;
} else
{ return 2.0m;
}
} else // Overnight
{ return 0.75m;
}
}
}
可以用,但非常难读,也不好改。
2.3.1 使用模式匹配以及其他技巧来简化代码
仅仅使用模式匹配来匹配所有可能性也不好,依然复杂,因为我们有很多种组合情况。
2.3.1.1 周末还是工作日
第一个条件是是否为周末。那么专门为此写一个函数:
// 注意 timeOfToll.DayOfWeek 和 DayOfWeek.Monday 中的 DayOfWeek 不是一个东西。// 前者是 DateTime 类型的一个属性,后者是一个 enum 类型。// 前者的值也为 DayOfWeek 类型public static bool IsWeekday(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch {
DayOfWeek.Monday => true,
DayOfWeek.Tuesday => true,
DayOfWeek.Wednesday => true,
DayOfWeek.Thursday => true,
DayOfWeek.Friday => true,
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false
}
还可以再简化:
public static bool IsWeekday(DateTime timeOfToll) =>
timeOfToll.DayOfWeek switch {
DayOfWeek.Saturday => false,
DayOfWeek.Sunday => false,
_ => true
}
2.3.1.2 一天的时间段
先看代码:
public enum TimeBand
{
MorningRush,
Daytime,
EvenignRush,
Overnight
}public static TimeBand GetTimeBand(DateTime timeOfToll) =>
timeOfToll.Hour switch
{
> 19 or < 6 => TimeBand.Overnight,
< 10 => TimeBand.MorningRush,
> 16 => TimeBand.EvenignRush,
_ => TimeBand.Daytime
};
使用了 enum 来将一天的多个时间段分配值。
使用了
> 19 or < 6
这种语法,>
和<
以及or
都是在 C# 9.0 后引入的。当然还有>=
,<=
,and
,not
这些语法。(什么你问为什么没有=
的语法,因为不需要,直接写 6 就是 =6 了)
2.3.1.3 最终代码
有了以上两个函数后,代码就可以简化为这种 tuple pattern 形式:
public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, true) => 1.50m,
(true, TimeBand.Daytime, false) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, true) => 0.75m,
(true, TimeBand.Overnight, false) => 0.75m,
(false, TimeBand.MorningRush, true) => 1.00m,
(false, TimeBand.MorningRush, false) => 1.00m,
(false, TimeBand.Daytime, true) => 1.00m,
(false, TimeBand.Daytime, false) => 1.00m,
(false, TimeBand.EveningRush, true) => 1.00m,
(false, TimeBand.EveningRush, false) => 1.00m,
(false, TimeBand.Overnight, true) => 1.00m,
(false, TimeBand.Overnight, false) => 1.00m,
};
当然,很多条件可以简化:
public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.MorningRush, false) => 1.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, true) => 1.00m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
(false, _, _) => 1.00m,
};
然后可以把 3 个返回 1.00m 的用 _ 代替:
public static decimal CalculateToll(DateTime timeOfToll, bool isInbound) =>
(IsWeekday(timeOfToll), GetTimeBand(timeOfToll), isInbound) switch
{
(true, TimeBand.MorningRush, true) => 2.00m,
(true, TimeBand.Daytime, _) => 1.50m,
(true, TimeBand.EveningRush, false) => 2.00m,
(true, TimeBand.Overnight, _) => 0.75m,
_ => 1.00m,
};