從零開始獨(dú)立游戲開發(fā)學(xué)習(xí)筆記(十七)--Unity學(xué)習(xí)筆記(六)--微軟C#指南(三)

好了,忙的時(shí)候結(jié)束了。
繼續(xù)講述和對象相關(guān)的知識。這一章講使用模式匹配進(jìn)行類型轉(zhuǎn)換。
1. 如何安全地格式轉(zhuǎn)換(模式匹配)
由于對象具有多態(tài)性。一個(gè)具有基類類型的變量是可以存放 derived 類型的變量的值的,但這有可能產(chǎn)生 InvallidCastException。C# 提供了使用模式匹配(pattern match)的格式轉(zhuǎn)換(cast),僅當(dāng)成功的時(shí)候會轉(zhuǎn)換。C# 也提供了?is
?和?as
?關(guān)鍵字來判斷一個(gè)值是否為某個(gè)類型。
1.1?is
?運(yùn)算符
比如說以下代碼:
static void FeedMammal(Animal a) { ? ?if (a is Mammmal m)
? ?{
? ? ? ?m.Eat();
? ?} ? ?else
? ?{
? ? ? ?Console.WriteLine($"{a.GetType().Name} is not a Mammal");
? ?}
}
重點(diǎn)在于:
is
?后面并不只是一個(gè)類型,而是聲明了一個(gè) Mammal 類型的變量。并不是說只能這么寫。單單寫?a is Mammal
?也行,只是這種語法把類型判斷和初始化寫在一起,也是可行的一種語法。當(dāng)判斷成功的時(shí)候,a 的值會被賦予給了 m。m 的作用域僅僅在于 if 里,甚至連 else 里都無法訪問。
1.2?as
?運(yùn)算符
請看以下代碼:
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
?運(yùn)算符執(zhí)行一次轉(zhuǎn)換。如果成功則轉(zhuǎn)換成對應(yīng)類型,不成功則返回 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. 模式匹配的場景
現(xiàn)代開發(fā)經(jīng)常要用到來自各種不同地方的數(shù)據(jù)源,因此數(shù)據(jù)類型也都不一致。
于是文章采用了這么一個(gè)場景--在一個(gè)收費(fèi)站收費(fèi)。根據(jù)高峰期和車型收費(fèi)。 難點(diǎn)在于,數(shù)據(jù)來源可能是多個(gè)不同的外部系統(tǒng)。那么首先假設(shè)有這么三個(gè)系統(tǒng)(3 個(gè) 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; }
? ?}
}
即,數(shù)據(jù)可能以不同的 class 形式存在。
2.1 最基礎(chǔ)的收費(fèi)
寫一個(gè)最基礎(chǔ)的收費(fèi)類:
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))
? ? ? ?};
? ?}
}
這里使用了一個(gè) switch expression 的語法(非 switch statement)。語法一看大概也知道是怎么回事。因?yàn)檎麄€(gè)是一個(gè) switch,因此 => 跟的就是 return 的值。
{ } 則是匹配所有的 非 null 的 object。必須寫在后面,否則就被第一個(gè)返回了。
null 則是匹配 null。
2.2 根據(jù)乘客收費(fèi)
為了減少流量,讓車輛載客數(shù)更高,因此希望乘客越少收費(fèi)越高。
我們可以改寫上面的代碼:
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
?的用法也是簡潔明了。當(dāng)并等于某一個(gè)值,而是一個(gè)判斷語句的時(shí)候用 when。以上的代碼有部分比較重復(fù)。比如對于 car 和 taxi,每個(gè)乘客數(shù)量都要寫一整行代碼??梢员缓喕癁橐韵麓a:
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))
? ?};
可以看到根本沒有新的語法。而是再寫一個(gè) switch expression。
_ 表示匹配其他所有情況。同理也不能寫在前面,因?yàn)橐欢〞黄ヅ渖稀?/p>
2.3 根據(jù)高峰時(shí)間收費(fèi)
假設(shè)有這么一個(gè)需求。周末正常收費(fèi)。工作日的話,早上的入流量和晚上的出流量雙倍收費(fèi)。其他時(shí)間 1.5 倍收費(fèi)。凌晨則減少為 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 使用模式匹配以及其他技巧來簡化代碼
僅僅使用模式匹配來匹配所有可能性也不好,依然復(fù)雜,因?yàn)槲覀冇泻芏喾N組合情況。
2.3.1.1 周末還是工作日
第一個(gè)條件是是否為周末。那么專門為此寫一個(gè)函數(shù):
// 注意 timeOfToll.DayOfWeek 和 DayOfWeek.Monday 中的 DayOfWeek 不是一個(gè)東西。// 前者是 DateTime 類型的一個(gè)屬性,后者是一個(gè) 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 一天的時(shí)間段
先看代碼:
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 來將一天的多個(gè)時(shí)間段分配值。
使用了?
> 19 or < 6
?這種語法,>
?和?<
?以及?or
?都是在 C# 9.0 后引入的。當(dāng)然還有?>=
,<=
,and
,not
?這些語法。(什么你問為什么沒有?=
?的語法,因?yàn)椴恍枰?,直接?6 就是 =6 了)
2.3.1.3 最終代碼
有了以上兩個(gè)函數(shù)后,代碼就可以簡化為這種 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,
? ?};
當(dāng)然,很多條件可以簡化:
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 個(gè)返回 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,
? ?};