【问题标题】:Convert a class that has a string property into another class with typed property value将具有字符串属性的类转换为具有类型属性值的另一个类
【发布时间】:2019-09-05 16:07:57
【问题描述】:

有以下类:

public class DeviceParameter
{
    public string Key { get; set; }

    public Guid DeviceId { get; set; }

    public string Value { get; set; }
}

一个设备可以有很多不同类型的参数,但它们都以字符串的形式存储在数据库中。

public abstract class DeviceValueTypedParameter<TValue>
{
    public string CodeName { get; }

    public TValue Value { get; set; }

    public Guid DeviceId { get; set; }

    public DeviceValueTypedParameter(string codeName)
    {
        this.CodeName = codeName;
    }
}

DeviceValueTypedParameter 是一个抽象,有一个类型化的值 (TValue) 用于C#的参数值,而不是使用我们从数据库中获取的字符串。 DeviceValueTypedDeviceParameter 和 DeviceParameter 之间没有继承关系,因为我想通过组合将 TValue 转换为字符串。

public class ArmingStatusParameter : DeviceValueTypedParameter<ArmingStatuses>
{
    public const string CODE_NAME = "ArmingStatus";

    public ArmingStatusParameter() : base(CODE_NAME)
    {
    }
}

public enum ArmingStatuses
{
    Unknown,
    Armed,
    Disarmed,
}

ArmingStatusParameter 是可以存在的类型化参数的示例,其中值是 ArmingStatuses 的枚举。其他可以存在的类型有 DateTimes、int32、double 等。

我已经完成了从类型值到字符串的转换,但是现在我正在努力如何正确地进行从字符串到类型值的转换。

尝试了不同的方法:

  1. 隐式或显式转换
  2. 扩展方法
  3. 存在的每种类型的转换器类
  4. 基于 TValue 类型的通用转换器类

方案一:容易实现,但违反了
的POCO 武装状态参数。人们可能会忘记实现隐式/显式运算符,错误只会在编译时发生。

选项 2:违反接口隔离原则 (ISP),因为需要直接访问转换。

选项 3:可行,但人们将不得不创建很多类并且代码会过于冗长。对于每个不同的参数,都需要实例化一个新的 {X}TypedParameterConverter。

选项 4:似乎是最好的选择,但我在“使其发挥作用”方面遇到了麻烦

我在想这样的事情:

public interface IDeviceValueTypedParameterConverter
{
    bool TryConvert<T, TValue>(DeviceParameter deviceParameter, 
        DeviceValueTypedParameter<TValue> deviceValueTypedParameter)
        where T : DeviceValueTypedParameter<TValue>;
}

public class DeviceValueTypedParameterConverter : IDeviceValueTypedParameterConverter
{
    public bool TryConvert<T, TValue>(DeviceParameter inputParameter, 
            DeviceValueTypedParameter<TValue> outputParameter)
            where T : DeviceValueTypedParameter<TValue>
    {
        bool result = true;
        if (inputParameter == null)
        {
            throw new NullReferenceException($"DeviceValueTypedParameter:'{typeof(T)}' must be initialized first");
        }

        if (inputParameter.Value is int)
        {
            result = int.TryParse(inputParameter.Value, out int temp);
            outputParameter.Value = (TValue)temp;
        }
        else if (inputParameter.Value is Enum)
        {
            // some other code to convert the Enum's
        }
        // more else ifs one for each type 
        // (...)
        else
        {
            result = false;
        }
        outputParameter.DeviceId = inputParameter.DeviceId;
        return result;
    }
}

问题:

  • 所有 Ifs 都给我一个警告说:“给定的表达式永远不是提供的”。
  • 无法进行强制转换 (TValue)。它说不能将 int 转换为 TValue。唯一的解决方案是通过反思创造价值?

【问题讨论】:

  • 我认为您正在寻找 ISerializable 接口。在需要存储为文本的类中实现这一点。
  • 只有属性值存储在字符串中,我在转换为字符串时没有问题,问题是当我检索此值时如何从 DeviceParameter 转换为 DeviceValueTypedParameter从数据库。它们不是同一个类,这里​​不能使用序列化。
  • 我会重新考虑您正在尝试做的事情,这样您甚至都没有尝试解决这个问题。听起来您已经确定了它导致的一些问题。你能重新考虑一下,让它可序列化的吗?
  • DeviceParameter.Value 必须始终是字符串,所以我看不出您的 is 测试如何工作?另外,您的 out 参数不需要out 吗? DeviceParameter.Key 是否提示您有关 Value 的类型?
  • @ScottHannen 因为我无法更改数据库或值在数据库中的存储方式,所以我不知道在这种情况下如何使用序列化,即使如此,它也没有解决不了所有问题,只解决TValue转换问题。

标签: c# generics type-conversion


【解决方案1】:

这是我的尝试 - 我不确定它是否违反了您未解释(或已解释)的某些细节。由于out 参数不能使用多态性,所以我创建了一个接口来表示跨类型参数基类的常用函数。由于没有静态虚拟方法,因此我使用对象方法并创建了一个结果对象,如果可以转换,将使用该对象。

我认为转换方法没有理由有多个实例或需要一个接口,因此我将其创建为单个静态方法。我使用enum 来捕获可从传入类型访问的参数所需的转换类型,并且必须通过object 进行棘手的转换以处理对out 参数值字段的分配,因为C# 没有分配的类型切换能力。请注意,如果 IsPossible 方法没有正确过滤所有案例并且 ChangeType 失败,这可能会导致运行时错误。

public enum ValueParseTypes {
    Enum,
    DateTime,
    Int
}

public interface IDeviceValueTypedDeviceParameter<TValue> {
    string CodeName { get; }
    TValue Value { get; set; }
    Guid DeviceId { get; set; }
    ValueParseTypes ParseType { get; set; }

    bool IsPossibleValue(DeviceParameter aValue);
}

public abstract class DeviceValueTypedDeviceParameter<TValue> : IDeviceValueTypedDeviceParameter<TValue> {
    public string CodeName { get; }
    public TValue Value { get; set; }
    public Guid DeviceId { get; set; }
    public ValueParseTypes ParseType { get; set; }

    public DeviceValueTypedDeviceParameter(string codeName, ValueParseTypes parseType) {
        this.CodeName = codeName;
        this.ParseType = parseType;
    }

    public virtual bool IsPossibleValue(DeviceParameter aValue) => false;
}

public class ArmingStatusParameter : DeviceValueTypedDeviceParameter<ArmingStatuses> {
    public const string CODE_NAME = "ArmingStatus";

    public ArmingStatusParameter() : base(CODE_NAME, ValueParseTypes.Enum) {
    }

    static HashSet<string> ArmingStatusesNames = Enum.GetNames(typeof(ArmingStatuses)).ToHashSet();
    public override bool IsPossibleValue(DeviceParameter aValue) => ArmingStatusesNames.Contains(aValue.Value);
}

public enum ArmingStatuses {
    Unknown,
    Armed,
    Disarmed,
}

public class PoweredOnStatusParameter : DeviceValueTypedDeviceParameter<DateTime> {
    public const string CODE_NAME = "PoweredOn";

    public PoweredOnStatusParameter() : base(CODE_NAME, ValueParseTypes.DateTime) {
    }

    public override bool IsPossibleValue(DeviceParameter aValue) => DateTime.TryParse(aValue.Value, out _);
}

public class VoltageStatusParameter : DeviceValueTypedDeviceParameter<int> {
    public const string CODE_NAME = "PoweredOn";

    public VoltageStatusParameter() : base(CODE_NAME, ValueParseTypes.Int) {
    }

    public override bool IsPossibleValue(DeviceParameter aValue) => Int32.TryParse(aValue.Value, out _);
}

public static class DeviceValueTypedParameterConverter {
    public static bool TryConvert<TValue>(DeviceParameter inputParameter, IDeviceValueTypedDeviceParameter<TValue> outputParameter)
            where TValue : struct {
        if (inputParameter == null)
            throw new ArgumentNullException(nameof(inputParameter));
        else if (outputParameter == null)
            throw new ArgumentNullException(nameof(outputParameter));

        bool result = false;
        if (outputParameter.IsPossibleValue(inputParameter)) {
            outputParameter.DeviceId = inputParameter.DeviceId;
            switch (outputParameter.ParseType) {
                case ValueParseTypes.Enum:
                    if (Enum.TryParse(inputParameter.Value, out TValue typedValue)) {
                        outputParameter.Value = typedValue;
                        result = true;
                    }
                    break;
                case ValueParseTypes.DateTime:
                    if (DateTime.TryParse(inputParameter.Value, out var dtValue)) {
                        outputParameter.Value = (TValue)Convert.ChangeType(dtValue, typeof(TValue));
                        result = true;
                    }
                    break;
                case ValueParseTypes.Int:
                    if (Int32.TryParse(inputParameter.Value, out var intValue)) {
                        outputParameter.Value = (TValue)Convert.ChangeType(intValue, typeof(TValue));
                        result = true;
                    }
                    break;
            }
        }

        return result;
    }
}

现在你可以像这样使用它了:

var as_tv = new DeviceParameter() {
    Key = "testkey",
    DeviceId = new Guid(),
    Value = "Armed"
};

var asp = new ArmingStatusParameter();
if (DeviceValueTypedParameterConverter.TryConvert<ArmingStatuses>(as_tv, asp)) {
    // work with asp
}

var po_tv = new DeviceParameter() {
    Key = "testkey2",
    DeviceId = new Guid(),
    Value = "4/15/2019 17:36"
};

var pop = new PoweredOnStatusParameter();
if (DeviceValueTypedParameterConverter.TryConvert<DateTime>(po_tv, pop)) {
    // work with pop
}

var v_tv = new DeviceParameter() {
    Key = "testkey3",
    DeviceId = new Guid(),
    Value = "17"
};

var vp = new VoltageStatusParameter();
if (DeviceValueTypedParameterConverter.TryConvert<int>(v_tv, vp)) {
    // work with vp
}

【讨论】:

  • 谢谢。我将尝试根据这些原则进行实施。唯一困扰我的是 TryConvert 方法需要传递 2T,而第二个 T(TValue) 可能会错误地传递。有什么方法可以强制 TValue “始终正确”或将其删除?该 TValue 已经存在于 DeviceValueTypedDeviceParameter 的实例中。我认为使用对象并在运行时获取类型是可能的,但我想知道是否可以不进行反射。
  • 不可能有T 是例如ArmingStatusParameter 并弄清楚 TValue 应该是什么,但您可以做相反的事情(仅传递 TValue),但必须在调用站点创建和传递返回值。我已经更新了代码。
猜你喜欢
  • 2021-12-04
  • 2014-01-13
  • 1970-01-01
  • 2015-06-27
  • 1970-01-01
  • 2011-04-12
  • 1970-01-01
  • 2012-07-17
  • 1970-01-01
相关资源
最近更新 更多