如何打开System.Type?

109
在 C# 7+ 中,我能直接使用 System.Type 进行 switch 吗?
当我尝试这样做时:
    switch (Type)
    {
      case typeof(int):
        break;
    }

它告诉我 typeof(int) 需要是一个常量表达式。

有没有一些语法糖可以让我避免使用 case nameof(int): 并直接比较类型是否相等? case 语句中的 nameof(T) 并不完全好,因为存在命名空间。因此,尽管对于int可能不适用名称冲突,但对于其他比较则适用。

换句话说,我正在尝试比这更加类型安全:

    switch (Type.Name)
    {
      case nameof(Int32):
      case nameof(Decimal):
        this.value = Math.Max(Math.Min(0, Maximum), Minimum); // enforce minimum 
        break;
    }

6
@Servy,不是的,我不这样认为。我的例子不是答案,而是反例。 - toddmo
5
你可以构建一个 Dictionary<Type, Action>,以调用特定类型的函数,而不是使用开关语句。我认为这样编写代码更加简洁。 - xxbbcc
6
“告诉我typeof(int)需要是常量表达式。”这就是你问题的答案。 - Servy
3
在这里描述了:https://blogs.msdn.microsoft.com/dotnet/2016/08/24/whats-new-in-csharp-7-0/,使用模式的switch语句。 - Matthias247
3
或许你可以解释一下为什么要在 System.Type 上进行切换,而不是直接在值上进行切换?我猜在此之前可能需要使用 typeof(value) - Matthias247
显示剩余10条评论
10个回答

131
新的模式匹配功能(已链接)可以实现这一点。
通常,您会打开一个值:
switch (this.value) {
  case int intValue:
    this.value = Math.Max(Math.Min(intValue, Maximum), Minimum);
    break;
  case decimal decimalValue:
    this.value = Math.Max(Math.Min(decimalValue, Maximum), Minimum);
    break;
}

但如果您只有一个类型,仍然可以使用它来切换类型:

switch (type) {
  case Type intType when intType == typeof(int):
  case Type decimalType when decimalType == typeof(decimal):
    this.value = Math.Max(Math.Min(this.value, Maximum), Minimum);
    break;
}

请注意,这不是该特性的预期用途,与传统的 if...else if...else if...else 链相比,它变得不太可读,并且编译器最终还是将其编译为传统链式结构。我不建议像这样使用模式匹配。

1
你能够链接多个 when 吗?而不是使用 case Type t when (t == typeof(int) || t == typeof(decimal)) : - toddmo
1
@toddmo(回应您编辑的评论)使用||时,它不是链式的,但任何条件都是有效的,包括由多个子表达式组成并使用&&||和任何其他运算符组合的条件。 - user743382
3
为什么要声明新的“Type”变量,而不是使用丢弃模式:“case var _ when type == typeof(int)”? - user4003407
在一般情况下:为了防止多次评估开关值。在这种情况下:我不知道是否更好,但它可能至少不会比我在答案中提供的更糟。不过我仍然不会使用任何一个。 - user743382

60
这里OP提出的问题是,当您没有实际的类型可用,而只有其假定的System.Type时,您无法使用新的C# 7基于类型的switch功能。接受的答案总结如下,对于精确类型匹配效果很好(这里显示了一点改进,但请参见下面的最终示例以获得更进一步的简化)...
Type type = ...
switch (type)
{
    case Type _ when type == typeof(Int32):
    case Type _ when type == typeof(Decimal):
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum);
        break;
}

需要注意的重要一点是,对于派生的引用类型层次结构,这将不会展示与使用"is"关键字进行匹配的"if...else"链相同的行为。请考虑:
class TBase { }
class TDerived1 : TBase { }
class TDerived2 : TBase { }
sealed class TDerived3 : TDerived2 { }

TBase inst = ...

if (inst is TDerived1)
{
    // Handles case TDerived1
}
else if (inst is TDerived2)
{
    // Handles cases TDerived2 and TDerived3
}
else if (inst is TDerived3)
{
    // IMPOSSIBLE (never executed)               <---  !
}

由于`TDerived3`是`TDerived2`的一种,当使用`is`匹配时,两种情况都由第二个分支处理。由于在此示例中`TDerived3`被标记为“sealed”,因此没有实例能够匹配第三个分支。
这突显了运行时“严格”或“精确”的类型相等与更微妙的“类型包容性”(类型“兼容性”)之间的差异。由于OP问题中的类型是`ValueType`原始类型(无法派生自其他类型),所以这种差异并不重要。但是,如果我们将接受的答案中的“精确类型匹配”与上面显示的示例类结合起来,我们将得到不同的结果。
Type type = ...

switch (type)
{
    case Type _ when type == typeof(TDerived1):
        // Handles case TDerived1
        break;

    case Type _ when type == typeof(TDerived2):
        // Handles case TDerived2
        break;

    case Type _ when type == typeof(TDerived3):
        // Handles case TDerived3                 <---  !
        break;
}

实际上,C# 7甚至不会编译与之前显示的if / else序列相对应的switch语句。(注:看起来编译器应该将此视为警告,而不是错误,因为无害的结果只是一段无法访问的代码分支,这是编译器在其他地方视为警告的条件,并且还考虑到编译器在if / else版本中甚至根本不检测到看似相同的情况)。这是代码示例:

enter image description here

无论如何,哪种替代行为是合适的,或者是否重要,都取决于您的应用程序,所以我在这里的观点只是为了引起注意。如果您确定需要更精明的“类型兼容性”版本的开关方法,以下是如何操作的:
Type type = ...

switch (type)
{
    case Type _ when typeof(TDerived1).IsAssignableFrom(type):
        // Handles case TDerived1
        break;

    case Type _ when typeof(TDerived2).IsAssignableFrom(type):
        // Handles cases TDerived2 and TDerived3
        break;

    case Type _ when typeof(TDerived3).IsAssignableFrom(type):
        // IMPOSSIBLE (never executed)            <---  !
        break;
}

和之前一样,第三个案例是多余的,因为对于当前示例,TDerived3 是密封的。

最后,正如我在本页面的另一个answer中提到的,你甚至可以进一步简化对switch语句的使用。由于我们只使用了when子句的功能,并且由于我们可能仍然在一个变量中拥有原始的被切换的Type实例,所以在switch语句中不需要提及该变量,也不需要在每个case中重复其类型(在这种情况下为Type)。只需执行以下操作:

Type type = ...

switch (true)
{
    case true when typeof(TDerived1).IsAssignableFrom(type):
        break;

    case true when typeof(TDerived2).IsAssignableFrom(type):
        break;

    case true when typeof(TDerived3).IsAssignableFrom(type):
        break;
}

注意到switch(true)case(true)。我建议在仅依赖when子句(即除了在此处讨论的基于System.Type的情况之外)时使用这种更简单的技术。

我认为你应该像这样交换这两个东西。应该是:typeof(TDerivedN).IsAssignableFrom(type)。并且将情况#3放在第一位。 - N73k
@N73k,我针对IsAssignableFrom指定的顺序是正确的。这是对更少派生 Type的实例方法调用。正如你所指出的,交换顺序直接关系到case语句的顺序,因此也是按照指定的方式正确的。 - Glenn Slayden
嗯,我不这么认为。例如,如果类型是TDerived4(其中TDerived4:TDerived3),那么您希望触发最后一行(带有“TDerived3”的那一行)。但是没有任何情况语句被触发,因为TDerived4不能从任何情况语句中的任何TDerived分配。 - N73k
@N73k 我的错。我更新了答案以纳入您的更正。 - Glenn Slayden

22

从Paulustrious提出的打开一个常量的想法开始,但是要追求最大的可读性:

  Type type = GetMyType();
  switch (true)
  {
    case bool _ when type == typeof(int):
      break;
    case bool _ when type == typeof(double):
      break;
    case bool _ when type == typeof(string):
      break;
    default:
      break;
  }

可读性因人而异。很久以前我曾在VB中做过类似的事情,所以我习惯于这种形式(但在VB中不需要bool _,所以它不存在)。不幸的是,在c#中需要使用bool _。我正在使用c# 7.0,我认为在早期编译器中可能不支持基于常量的切换,但我对此并不确定,如果您想尝试,请自行验证。我觉得挺有趣的,S/O代码格式化程序还不知道when的存在。

当然,如果需要为子类设置case变量,则不应该这样做。

但是对于任意的布尔表达式,这种方式更加适合,例如:

  switch (true)
  {
    case bool _ when extruder.Temperature < 200:
      HeatUpExtruder();
      break;
    case bool _ when bed.Temperature < 60:
      HeatUpBed();
      break;
    case bool _ when bed.Y < 0 || bed.Y > 300:
      HomeYAxis();
      break;
    default:
      StartPrintJob();
      break;
  }

有人会认为这比 if..else 更糟。我能说的是,switch 强制只走一条路线,并且不可能打破 switch 语句本身,但是,如果无意中遗漏了一个 else,就有可能把 if..else 分成多个语句并执行两个“分支”。

根据变量属性进行 Type 的切换实际上很随意,因为我们真正要切换的是变量的属性。除非我们可以使用 case typeof(int)(在非常量表达式上使用 case),否则,如果不想使用字符串常量,在类型名称的情况下,我们就被卡住了,因为它们不在框架中。


2
你已经基本涵盖了它。这纯粹是 if-then-else-if 的另一种方式。但是如果我们采用这个概念,任何 switch 语句的目的是什么?它提高了可读性,不太可能忘记一个“not”,并且如你所说,可以捕获缺少 else 的情况。FYI,C# V7 是第一个支持此功能的版本,也是第一个 case 语句顺序有所不同的版本。我了解你的 VB。我用过 Powerbuilder,它有一个 choose 语句,可以给你类似的功能。最后,when 子句中可以有多个 'ifs'。 - Paulustrious
1
我在使用自己的类型时遇到了同样的问题,这个实现确实帮助整理了一堆混乱的if-else-if-else语句。 - Daniel

16

虽然问题涉及到C# 7,但对于拥有C# 8或更高版本的人,可以使用更新的语法,它更加简短,我认为也更好看。

type switch
{
    Type _ when type == typeof(int) || type == typeof(decimalType) =>
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum),
    _ => // default case
};

另一个优雅的选项:

Type.GetTypeCode(type) switch
{
    TypeCode.Int32 or TypeCode.Decimal =>
        this.value = Math.Max(Math.Min(this.value, Maximum), Minimum),
    _ => // default case
};

更多信息请参见: https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/switch-expression


4

@toddmo建议如下:

switch (true)
{
    case bool _ when extruder.Temperature < 200:
        HeatUpExtruder();
        break;

    // etc..
    default:
        StartPrintJob();
        break;
}

......但为什么不在追求简单的过程中更进一步呢。以下代码同样可以运行,而不需要bool类型限定符,也不需要多余的_虚拟变量:

switch (true)
{
    case true when extruder.Temperature < 200:
        HeatUpExtruder();
        break;

    // etc.
    default:
        StartPrintJob();
        break;
}

"case true when" 比 "case bool _ when" 更易读。谢谢!" - Daniel

0

我知道这不适用于所有情况,但也许我的例子能帮助到某些人。在ASP.NET Core中,我实现了自定义模型绑定程序,并且我必须根据模型类型解析绑定器类型。

最初的想法(除了if/else块外,但我一直在考虑它可能会更短)是使用switch:

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        switch (context.Metadata.ModelType)
        {
            case Type _ when context.Metadata.ModelType == typeof(Model1):
                return new BinderTypeModelBinder(typeof(Binder1));
            case Type _ when context.Metadata.ModelType == typeof(Model2):
                return new BinderTypeModelBinder(typeof(Binder2));
            case Type _ when context.Metadata.ModelType == typeof(Model3):
                return new BinderTypeModelBinder(typeof(Binder3));
        }

        return null;
    }

这就像是一个字典:

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var bindersByType = new Dictionary<Type, Type>
        {
            {typeof(Model1),  typeof(Binder1)},
            {typeof(Model2),  typeof(Binder2)},
            {typeof(Model3),  typeof(Binder3)}
        };

        return bindersByType.TryGetValue(context.Metadata.ModelType, out Type binderType) ? new BinderTypeModelBinder(binderType) : null;
    }

这个想法的功劳归功于@xxbbcc,在第一个问题的评论中发布了这个想法。


0

补充以上答案,如果您不需要在case语句内使用该值,可以使用_。这种语法可以用于消除未使用变量的警告信息。

switch (this.value) {
  case int _:
    //Do something else without value.
    break;
  case decimal decimalValue:
    this.value = Math.Max(Math.Min(decimalValue, Maximum), Minimum);
    break;
}

-1

我找到了一种简单而高效的方法。 它需要 C# V7 运行。 switch("") 表示所有情况都会满足,直到 when 子句。 它使用 when 子句来查看 type

List<Object> parameters = new List<object>(); // needed for new Action
parameters = new List<object>
{
    new Action(()=>parameters.Count.ToString()),
    (double) 3.14159,
    (int) 42,
    "M-String theory",
    new System.Text.StringBuilder("This is a stringBuilder"),
    null,
};
string parmStrings = string.Empty;
int index = -1;
foreach (object param in parameters)
{
    index++;
    Type type = param?.GetType() ?? typeof(ArgumentNullException);
    switch ("")
    {
        case string anyName when type == typeof(Action):
            parmStrings = $"{parmStrings} {(param as Action).ToString()} ";
            break;
        case string egStringBuilder when type == typeof(System.Text.StringBuilder):
            parmStrings = $"{parmStrings} {(param as System.Text.StringBuilder)},";
            break;
        case string egInt when type == typeof(int):
            parmStrings = $"{parmStrings} {param.ToString()},";
            break;
        case string egDouble when type == typeof(double):
            parmStrings = $"{parmStrings} {param.ToString()},";
            break;
        case string egString when type == typeof(string):
            parmStrings = $"{parmStrings} {param},";
            break;
        case string egNull when type == typeof(ArgumentNullException):
            parmStrings  = $"{parmStrings} parameter[{index}] is null";
            break;
        default: throw new System.InvalidOperationException();
    };
} 

这使得parmStrings包含...

System.Action 3.14159, 42, M-String理论,这是一个stringBuilder,参数[5]为null


2
你正在使用字符串进行开关操作,而不是使用类型进行开关操作,这样做是否更快?因为这样做有点难以理解。看着它的人会犯愁一会儿,想知道case表达式的含义(事实证明它们对编译器没有任何意义,但变量名让它们看起来像是有意义的,就像注释一样使用它们,但这并不容易清楚地表达出来)。 - toddmo
@toddmo 你说得对。我已经查看了生成的代码,它很简单。比一些早期的解决方案要便宜得多。我没有考虑到变量名会引起混淆。但是一旦你看过并理解了它,下次遇到就会容易得多。我使用String变量名作为注释。假设有四个bool或bool?变量。我将称之为TTFN(N=null)的一个案例。接下来,例如TxxT是条件1和4为真,2和3无关。这使得查找变得容易,当然你还有默认值来捕捉你忘记的那些。 - Paulustrious
你应该添加生成的代码。那将非常有趣。无论如何,真正的问题比混淆维护开发人员更深入,而我现在还不够聪明来解释它。它涉及到做编译器不会强制执行的事情。我猜如果whenning变量不是System.Type,而是一些具有许多子类的基本类型,例如System.Exception,那么这将是一个更大的问题。无论如何,“注释变量”可能会与whenning变量不同步。如果你认为它们永远不会出错,那你还没有遇到我的同事哈哈。 - toddmo
@toddmo。我在我的示例中使用类型只是因为这是OP请求的。在另一个答案中,我会提供一些真实代码。然而,这并没有回答OP的问题,所以它可能会被删除。 - Paulustrious
请查看我的答案,我试图消除变量名称混淆和 switch 表达式混淆的缺点。 - toddmo

-1

这个方法可能会破坏你的代码约定,但是我还是使用了它。

var result = this.Value is int intValue
? Math.Max(Math.Min(intValue, Maximum), Minimum)
: this.Value is decimal decimalValue
? Math.Max(Math.Min(decimalValue, Maximum), Minimum)
: this.Value is double doubleValue
? Math.Max(Math.Min(decimalValue, Maximum), Minimum)
: throw new Exception($"Cannot handle '{this.Value.GetType().Name}' value.");

当情况更为适合时,您可以在条件语句中使用 Type.IsAssignableFrom()


它本质上并不是一个惯例;它只是字面上使用了 switch 语句。 - toddmo

-4

这里有一个替代方案,但由于缺少类而无法编译:

bool? runOnUI = queuedAction.RunOnUI;  // N=null, T=true F=False
bool isOnUI = Statics.CoreDispatcher.HasThreadAccess;
bool isFF = queuedAction.IsFireAndForget;   // just makes it easier to read the switch statement
if (false == queuedAction.IsFireAndForget)
    IsOtherTaskRunning = true;

/* In the case statements below the string name is something like noFF_TN
 * The compiler ignores the string name. I have used them here to represent
 * the logic in the case statement:   ( FF = FireAnd Forget, T=true, F=false, N = null, * means 'whatever')
 * 
 *      isFF_** = FireAndForget QueuedAction
 *      noFF_** = Not FireAndForget so Wait for Task to Finish (as opposed to Complete)
 *      ****_T* = We are already on the UI thread 
 *      ****_F* = We are not on the UI thread 
 *      ****_*T = Run on the UI thread.
 *      ****_*F = Do not run on UI thread
 *      ****_*N = Don't care so run on current thread
 *      
 *  The last character is a "bool?" representing RunOnUI. It has
 *  three values each of which has a different meaning */

bool isTask;
switch ("ignore")
{
    /* Run it as an Action (not Task) on current Thread   */
    case string noFF_TT when !isFF && isOnUI && runOnUI == true:
    case string isFF_TN when isFF && isOnUI && !runOnUI == null:
    case string isFF_FN when isFF && !isOnUI && runOnUI == null:
    case string isFF_TT when isFF && isOnUI && runOnUI == true:
    case string isFF_FF when isFF && !isOnUI && runOnUI == false:
        isTask = false;
        queuedAction.ActionQA(queuedAction); // run as an action, not as a Task
        break;

    /* Create a Task, Start it */

    case string isFF_TF when isFF && isOnUI == true && runOnUI == false:
    case string noFF_TN when !isFF && isOnUI == true && runOnUI == null:     // <== not sure if I should run it on UI instead
    case string noFF_TF when !isFF && isOnUI && runOnUI == false:
    case string noFF_FN when !isFF && !isOnUI && runOnUI == null:
    case string noFF_FF when !isFF && !isOnUI && runOnUI == false:
        var cancellationTokenSource = new CancellationTokenSource();
queuedAction.Canceller?.SetCancellationTokenSource(cancellationTokenSource);
        isTask = true;
        new Task
            (
                (action) => queuedAction.ActionQA(queuedAction),
                queuedAction,
                cancellationTokenSource.Token
            )
            .Start();
        break;

    /* Run on UI and don't wait */

    case string isFF_FT when isFF && !isOnUI && runOnUI == true:
        isTask = true;
        Statics.RunOnUI(() => queuedAction.ActionQA(queuedAction), asTaskAlways: true);
        break;

    /* Run on the UI as a Task and Wait */

    case string noFF_FT when !isFF && !isOnUI && runOnUI == true:
        isTask = true;
        Statics.RunOnUI(() => queuedAction.ActionQA(queuedAction), asTaskAlways: true);
        break;

    default:
        throw new LogicException("unknown case");
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接