在 C# 中,如何创建或重载赋值运算符以可能同时赋值两个值?

In C#, How can I create or overload an assignment operator to possibly assign two values at once?

提问人:Brian Kessler 提问时间:2/8/2023 最后编辑:Brian Kessler 更新时间:2/9/2023 访问量:187

问:

这可能是一个愚蠢的问题,但以防万一......

我们有一个第三方软件包,其中包含奇怪的模型,例如:

public partial class CountingDevice
{
    public int countingDeviceNo { get; set; }
    public string countingDeviceName { get; set; }
    public string obis { get; set; }
    public int integralPart { get; set; }
    public bool integralPartFieldSpecified;
    public int fractionalPart { get; set; }
    public bool fractionalPartFieldSpecified;
    public double value { get; set; }
    public bool valueFieldSpecified;
    public bool offPeakFlag { get; set; }
    public bool offPeakFlagFieldSpecified;
    public ExpectedMeterReading expectedMeterReading { get; set; }
    // snipped for brevity
}

您会注意到,有时存在成对的字段,例如 和 。integralPartintegralPartFieldSpecified

问题来了:如果我只是简单地分配一些值但不设置,则 的值将被完全忽略,从而导致解决方案失败。integralPartintegralPartFieldSpecified = trueintegralPart

因此,当将我们自己的模型映射到这种疯狂时,我需要在代码中加入以下结构:

if (IntegralPart != null)
{
    countingDevice.integralPartSpecified = true;
    countingDevice.integralPart = (int)IntegralPart!;
}

为了减少代码行数和不被雷区绊倒,我想做以下任何一项:

A. 重载运算符,以便它自动检查一个属性,该属性是布尔值,并且“指定”连接到当前属性的名称。如果存在此类属性,则在赋值时将为其赋值;如果没有,则分配将照常运行。理想情况下,它应该足够“聪明”,可以分配“......如果分配的值为 null/default/empty,则指定“设置为 false。=

B. 创建一些客户运算符,其操作与 A 相同。

C. 创建一些方法,我可以以简洁且最好是类型安全的方式调用这些方法来执行相同的操作。

这可能吗? 如果是这样,如何?

为了明确:我需要构建相当多的包装器。 我不想对每个字段重复此逻辑,并担心遗漏它适用的某些字段。 如果“指定”字段存在,我想要一种通用方法,可以一次分配两个字段,如果不存在,则能够以完全相同的方式进行分配。

C# 变量赋 值 赋值运 算符

评论

0赞 Mark Benningfield 2/8/2023
不能在 C# 中重载“=”运算符。对于模型类的扩展方法,您可能会有一些运气。
0赞 PMF 2/8/2023
@MakePeaceGreatAgain 这似乎来自第三方库,因此无法添加成员
0赞 MakePeaceGreatAgain 2/8/2023
然后创建一些设置这两个属性的方法?你需要使用 -operator 吗?还是在第三方模型上创建自己的模型?=
2赞 David 2/8/2023
@PMF:如果是这种情况,则不应将不受系统控制的第三方 DTO 用作域模型。有问题的系统可以在内部维护数据,但需要,只需在封装的第三方集成中转换/映射到此 DTO。核心问题似乎不在于运算符和属性,而在于在整个域中依赖于第三方组件。
0赞 PMF 2/8/2023
@David 同意,如果在整个系统中使用。但即使不是,也有一些地方需要映射数据。

答:

1赞 YungDeiza 2/8/2023 #1

不能在 C# 中重载运算符。=

您可以只使用自定义属性并在 setter 中设置“FieldSpecified”字段,例如

private int _integralPart;
public int integralPart 
{ 
    get { return _integralPart; }
    set
    {
        _integralPart = value;
        integralPartFieldSpecified = true;
    }
}
public bool integralPartFieldSpecified;

更新

如果你想要一个泛型解决方案,你可以将泛型类用于你想要实现指定行为的属性,例如:

public class ValueWithSpecifiedCheck<T>
{
    private T _fieldValue;

    public T FieldValue
    {
        get
        {
            return _fieldValue;
        }
        set
        {
            _fieldValue = value;
            FieldSpecified = true;
        }
    }

    public bool FieldSpecified { get; set; }
}

public class Data
{
    public ValueWithSpecifiedCheck<int> IntegralPart { get; set; }
}

然后,类/属性将按如下方式使用:

public static void Main()
{
    var data = new Data();

    data.IntegralPart = new ValueWithSpecifiedCheck<int>();
    data.IntegralPart.FieldValue = 7;
    Console.WriteLine(data.IntegralPart.FieldSpecified);// Prints true
}

评论

0赞 Brian Kessler 2/8/2023
你似乎忽略了一点,我不仅在处理 integralPart,而且在处理许多不同的领域,我想要一种通用的方式来处理这个问题,而不是重复逻辑。
0赞 YungDeiza 2/8/2023
明白了,增加了一个通用的解决方案
0赞 PMF 2/8/2023 #2

C# 不允许重载运算符(与 C++ 不同)。但是,您的建议 C 应该有效。这也有点麻烦,因为你必须编写一堆方法,但你可以编写一个扩展方法,例如=


public static class Extensions
{
    public static void UpdateIntegralPart(this CountingDevice dev, double value)
    {
        dev.integralPart = value;
        dev.integralPartSpecified = true;
    }
}

然后你可以打电话

   countingDevice.UpdateIntegralPart(1234);

评论

0赞 Brian Kessler 2/8/2023
你似乎忽略了一点,我不仅在处理 integralPart,而且在处理许多不同的领域,我想要一种通用的方式来处理这个问题,而不是重复逻辑。
1赞 PMF 2/8/2023
@BrianKessler 不,我看到了。但是,如果无法更改声明,并且仍然需要到处使用该类型,则除了创建扩展方法外,别无选择。也许你可以通过使用反射或类型来稍微改进重复,但这两者都不是类型安全的。dynamic
3赞 David 2/8/2023 #3

不绊倒雷区

封装雷区。

如果您不控制此第三方 DTO,则不要在整个域中使用它。将此第三方工具的集成封装或包装在您控制的黑匣子中。然后在整个中使用您的模型。

在此第三方系统的集成组件中,只需映射到您的域模型和此第三方 DTO。因此,在 DTO 上设置第二个字段的这一行额外代码仅存在于该位置。

评论

0赞 Brian Kessler 2/8/2023
封装雷区是我正在努力做的事情。但这是一个很大的雷区,我想以同样的方式处理其中的每一个变量,而不必担心我是否错过了一个地雷。
1赞 David 2/8/2023
@BrianKessler:重载运算符很可能是执行此操作的错误工具。继续类比...您在问题中提出的建议是穿过雷区,但封装一个危险的地雷。相反,封装整个字段。什么是第三方集成,您的系统如何使用它?您能否将其所有集成包装在一个服务类中?或者也许将此 DTO 包装在自定义模型中?您的代码将与包装器交互,包装器将在内部转换为此 DTO/从此 DTO。因此,这个值需要设置在一个地方,而不是其他地方。
1赞 Matthew Watson 2/8/2023 #4

如果实现泛型解决方案并添加隐式转换运算符,则使用起来非常方便。

下面是一个示例结构(我把它做成一个以确保不可变的机制):Optional<T>readonly struct

public readonly struct Optional<T> where T : struct
{
    public Optional(T value)
    {
        _value = value;
    }

    public static implicit operator T(Optional<T> opt) => opt.Value;
    public static implicit operator Optional<T>(T opt) => new(opt);

    public T Value => _value!.Value;

    public bool Specified => _value is not null;

    public override string ToString() => _value is null ? "<NONE>" : _value.ToString()!;

    readonly T? _value;
}

你可以用它来实现你的类,如下所示:CountingDevice

public partial class CountingDevice
{
    public int              countingDeviceNo   { get; set; }
    public string           countingDeviceName { get; set; }
    public string           obis               { get; set; }
    public Optional<int>    integralPart       { get; set; }
    public Optional<int>    fractionalPart     { get; set; }
    public Optional<double> value              { get; set; }
    public Optional<bool>   offPeakFlag        { get; set; }
    // snipped for brevity
}

由于隐式转换,使用是很自然的:

public static void Main()
{
    var dev = new CountingDevice
    {
        integralPart = 10,      // Can initialise with the underlying type.
        value        = 123.456
    };

    Console.WriteLine(dev.fractionalPart.Specified);  // False
    Console.WriteLine(dev.integralPart.Specified);    // True
    Console.WriteLine(dev.value);                     // 123.456
    Console.WriteLine(dev.value.ToString());          // 123.456
    Console.WriteLine(dev.fractionalPart.ToString()); // "<NONE>"

    dev.fractionalPart = 42;  // Can set the value using int.
    Console.WriteLine(dev.fractionalPart.Specified);  // True
    Console.WriteLine(dev.fractionalPart);            // 42

    var optCopy = dev.offPeakFlag;
    Console.WriteLine(optCopy.Specified);             // False

    dev.offPeakFlag = true;
    Console.WriteLine(dev.offPeakFlag.Specified);     // True

    Console.WriteLine(optCopy.Specified); // Still False - not affected by the original.

    Console.WriteLine(optCopy); // Throws an exception because its not specified.
}

您可能还希望使用可选的引用类型,但要做到这一点,您需要声明一个带有约束的泛型:class

public readonly struct OptionalRef<T> where T : class
{
    public OptionalRef(T value)
    {
        _value = value;
    }

    public static implicit operator T(OptionalRef<T> opt) => opt.Value;
    public static implicit operator OptionalRef<T>(T opt) => new(opt);

    public T Value => _value ?? throw new InvalidOperationException("Accessing an unspecified value.");

    public bool Specified => _value is not null;

    public override string ToString() => _value is null ? "<NONE>" : _value.ToString()!;

    readonly T? _value;
}

就我个人而言,我认为这有点矫枉过正。我只使用可为 null 的值类型等,但这取决于预期的用法。int?double?

1赞 Rufus L 2/9/2023 #5

另一个(昂贵的)解决方案是编写一个方法,该方法采用对象、属性名称和新属性值。然后,您可以使用反射来设置指定属性的属性值,以及搜索要设置的字段(如果存在)。bool

请注意,您需要传递属性的正确类型。例如,没有编译时检查您是否传递的是双精度值而不是属性的字符串。value

下面,我在类型上创建了一个扩展方法,以简化在主代码中调用该方法的过程(该方法成为对象本身的成员):object

public static class Extensions
{
    // Requires: using System.Reflection;
    public static bool SetPropertyAndSpecified(this object obj, 
        string propertyName, object propertyValue)
    {
        // Argument validation left to user

        // Check if 'obj' has specified 'propertyName' 
        // and set 'propertyValue' if it does
        PropertyInfo prop = obj.GetType().GetProperty(propertyName,
            BindingFlags.Public | BindingFlags.Instance);

        if (prop != null && prop.CanWrite)
        {
            prop.SetValue(obj, propertyValue, null);

            // Check for related "FieldSpecified" field 
            // and set it to 'true' if it exists
            obj.GetType().GetField($"{propertyName}FieldSpecified",
                BindingFlags.Public | BindingFlags.Instance)?.SetValue(obj, true);

            return true;
        }

        return false;
    }
}

将此类添加到项目后,可以执行如下操作:

static void Main(string[] args)
{
    var counter = new CountingDevice();

    // Note that 'valueFieldSpecified' and `integralPartFieldSpecified' 
    // are set to 'false' on 'counter'

    // Call our method to set some properties
    counter.SetPropertyAndSpecified(nameof(counter.integralPart), 42);
    counter.SetPropertyAndSpecified(nameof(counter.value), 69d);

    // Now 'valueFieldSpecified' and 'integralPartFieldSpecified' 
    // are set to 'true' on 'counter'
}

评论

0赞 Brian Kessler 2/9/2023
我想你是唯一一个了解我真正需要什么的人!下班后,同样的解决方案也发生在我身上(几乎是角色对角色)。我们可以通过两种方式来改善这一点吗?1. 限制为某些命名空间的成员?和 2.传递某种表达式来标识对象上的字段,而不是使用 ?我意识到编译时间检查是一个失败的原因。this object objstring propertyNameobject propertyValue
0赞 Rufus L 2/9/2023
当然,您可以通过将 更改为 来将方法限制为特定类型。this objectthis SomeOtherType
0赞 Brian Kessler 2/9/2023
也许这是一个新问题的主题,但有许多模型需要它,所有这些都来自同一群拥有自己命名空间的疯子。我不想将扩展限制为单个类,而是限制为命名空间中的所有类。
0赞 Rufus L 2/10/2023
我就是这么想的,这就是我使用.扩展方法适用于类型,而不是命名空间。如果所有类型都具有通用基础,则可以将其限制为类型。object
0赞 Brian Kessler 2/10/2023
不幸的是,我没有可以限制的基本类型。据我所知,C# 没有提供任何可以将接口改造到第三方模型的方法......