C# - 我可以在运行时设置泛型参数的类型来整理以下代码吗?

C# - Can I set the type of a generic parameter at runtime to tidy up the following code?

我有一个下面的例子类

public class Item<TMessageType> where TMessageType : ItemMessage
{
  public int Prop1 { get; set; }

  public string Prop2 { get; set; }

  public int MessageType { get; set; }
    
  public TMessageType Message { get; set; }

}
    

public class ItemMessage
{
  public int SomeProperty { get; set; }
}
    
public class TypeAMessage: ItemMessage
{
  public string PropA { get; set; }
}
    
public class TypeBMessage: ItemMessage
{
  public string PropB { get; set; }
}
    
public class TypeCMessage: ItemMessage
{
  public string PropC { get; set; }
}

我将从 'Items' 作为 JSON 的队列接收来自外部源的消息,并且在收到消息之前不知道项目消息的类型。 我可以通过原始 JSON 字符串上的正则表达式成功确定消息类型,并可以在 switch 语句中使用它来正确反序列化项目。例如

// get type 
...

// deserialise
dynamic item;
switch(messageTypeFromJson)
{
  case 0:
    item = JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString);
    break;

  case 1:
    item = JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString);
    break;

  case 2:
    item = JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString);
    break;

  default:
    // handle unexpected type
    return;
}

// use item

以上代码有效,但感觉 很乱。 我希望能够做一些更接近于以下(不起作用)的事情来将类型的确定和反序列化分成不同的步骤。

Type messageType;    
switch(messageTypeFromJson)
{
  case 0:
    messageType = typeof(TypeAMessage);
    break;

  case 1:
    messageType = typeof(TypeBMessage);
    break;

  case 2:
    messageType = typeof(TypeCMessage);
    break;

  default:
    // handle unexpected type
    return;
}

try
{
    var item = JsonSerializer.Deserialize<Item<messageType>>(jsonString);
    .....
}
catch(..){ ... }

有办法实现吗?

在你的 Item<> class 上没有基础 class 或接口,你能做的最好的事情就是处理 object

首先,在 switch 语句中使用完整的类型名称,而不是通用参数:

Type messageType;    
switch(messageTypeFromJson)
{
  case 0:
    messageType = typeof(Item<TypeAMessage>);
    break;
  case 1:
    messageType = typeof(Item<TypeBMessage>);
    break;
  case 2:
    messageType = typeof(Item<TypeCMessage>);
    break;
  default:
    // handle unexpected type
    return;
}

然后使用需要对象 Type:

的 Deserialize 的非泛型重载
var item = JsonSerializer.Deserialize(jsonString, messageType);

现在请务必注意,如果没有基数 type/interface,您就无法将 return 值直接 转换为任何值,将其保留为object。但是,您可以使用模式匹配来处理对象:

if(item is Item<TypeAMessage> ita) 
{
   // Do something 
} 
else if (item is Item<TypeBMessage> itb)
{
   // etc
} 

或者因为您知道类型(还记得开关吗?),您可以像这样使用它:

switch(messageTypeFromJson)
{
  case 0:
    Item<TypeAMessage> ita = (Item<TypeAMessage>)item;
    // Do something 
    break;
  case 1:
    // etc
    break;
}

或者,您可以反序列化为 JsonElementJsonDocument,然后直接使用“原始”JSON 类型。

我会注意到上面的内容非常重复。一种替代方案如下所示:

object item = messageTypeFromJson switch {
    0 => (object)JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString),
    1 => JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString), 
    2 => JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString), 
    _ => null
};

然后你可以打开类型(模式匹配)或 messageTypeFromJson 来决定你想用它做什么。

这完全取决于您打算用 item 做什么——您只是 return 还是需要访问它的属性?如果只是前者,那你就可以走了。如果您需要访问属性,您将需要一个非泛型基类型或接口。

public interface IItem
{
  public int Prop1 { get; set; }
  public string Prop2 { get; set; }
  public int MessageType { get; set; }
  public object Message { get; set; }
}

public interface IItem<TMessageType> : IItem where TMessageType : ItemMessage
{
  public new TMessageType Message { get; set; }
}

然后:

public class Item<TMessageType> : IItem<TMessageType> where TMessageType : ItemMessage
{
  public int Prop1 { get; set; }
  public string Prop2 { get; set; }
  public int MessageType { get; set; }
  object IItem.Message { get => Message; set => Message = value; } 
  public TMessageType Message { get; set; }
}

然后您就可以像这样使用它了:

var item = (IItem)JsonSerializer.Deserialize(jsonString, messageType);
// Do something with interface properties

或类似的:

IItem item = messageTypeFromJson switch {
    0 => (IItem)JsonSerializer.Deserialize<Item<TypeAMessage>>(jsonString),
    1 => JsonSerializer.Deserialize<Item<TypeBMessage>>(jsonString), 
    2 => JsonSerializer.Deserialize<Item<TypeCMessage>>(jsonString), 
    _ => null
};

然后您可以打开 Message 类型而不是 item:

if (item.Message is TypeAMessage tam)
{
   Console.WriteLine(tam.PropA);
   // etc
} 

警告:调用Deserialize时不要使用IItem作为泛型类型参数(或Type对象)。为什么?因为 System.Text.Json 只会反序列化该类型上存在的属性。这意味着您将 丢失 强类型 Message 数据和一些 它的 属性。

换句话说,不要这样做:

IItem item = JsonSerializer.Deserialize<IItem>(jsonString);

不推荐使用反射。

代码如下:

       //get generic deserialize method (there is two, we should filter to get the one we need.
        var deserializeMethod = typeof(JsonConvert).GetMethods().
            Where(x =>x.Name == nameof(JsonConvert.DeserializeObject)).
            Where(x =>x.IsGenericMethod).FirstOrDefault();

        var itemType = typeof(List<>/*item type here*/);
        var listGenericType = itemType.MakeGenericType(typeof(int)/*type from switch here*/);

        var genericMethod = deserializeMethod.MakeGenericMethod(listGenericType);

        var mappedObject = genericMethod.Invoke(null, new object[] { "[1,2,3]" /*your JSON here*/ });

你的方式更易读也更安全。

当使用 GetType() 方法或 typeof() 关键字时,您可以通过这种方式调用通用类型实例和方法。