【问题标题】:How to define two possible data type for property如何为属性定义两种可能的数据类型
【发布时间】:2021-02-21 14:15:13
【问题描述】:

我正在尝试在 C# 中为 2 个类定义运算符重载:FractionBasicFraction。 在外部,它们的行为相同,但在内部 Fraction 使用 PrimeFactorization(将数字存储为质因数列表),BasicFraction 使用 int 存储提名和分母。

他们会遵循这个接口(如果我使用 JSDoc 类型定义):

public interface IFraction
{
  public (int|Primefactoriztion) a; // nominator
  public (int|Primefactoriztion) b; // denominator
}

但既然我不能这样做,有没有一种简单的方法可以互换使用 Fraction 和 BasicFraction?
我可以定义所有运算符重载来评估intPrimeFactorization 之间的交互。但是,我不知道如何告诉 C# 接受两者都传递给方法。

Tl;dr:如何让一个属性允许 2 种数据类型?

注意事项:

  • 我是高级 C# 的新手(我知道静态类型和基本的 OOP,但仅此而已)。我来自 JS 和 Python 背景。
  • 我知道它们可能是具有此功能的优秀库。我写这篇文章是为了学习 C#,而不是为了生产。

编辑: 目前,PrimeFactorization 将因子及其幂存储在Dictionary<int, BasicFraction> 中(我会使用Fraction,但它会导致递归;这基本上是BasicFraction 的唯一用法)。

【问题讨论】:

  • 搜索c# Generics
  • 你能解释一下吗。你想让你的类的一种方法返回不同的类型吗?或者您想要一种方法获取两种不同的类型作为输入,但返回相同的类型?
  • @Ivan Khorin 简而言之,我想以不可更改的方式使用两个我知道具有兼容接口但无法定义编译器接口的类
  • 请注意,将 1 个属性用于 2 种数据类型的想法在 C# 中是的。虽然有几种方法可以做到这一点,但 C# 的心态是应尽可能依赖类型系统来维护正确的代码

标签: c# oop interface


【解决方案1】:

您想到的称为可区分联合,在 C# 中不能作为语言功能使用。

我会使用一种混合方法,其中分数将包含作为int 的提名者和分母以及作为只读属性的素数列表。您可以通过两个构造函数使用整数或素数列表初始化分数。丢失的条目将被延迟计算以最小化计算开销。

将属性设为只读可提高代码的稳健性。特别是如果您使用的是结构。请参阅Mutating readonly structs(Eric Lippert 的博客:编码中的精彩冒险)。但请注意,由于惰性求值,结构仍然是可变的。

public struct Fraction
{
    public Fraction(int nominator, int denominator)
    {
        _nominator = nominator;
        _denominator = denominator;
        _nominatorPrimeFactors = null;
        _denominatorPrimeFactors = null;
    }

    public Fraction(IList<int> nominatorPrimeFactors, IList<int> denominatorPrimeFactors)
    {
        if (nominatorPrimeFactors == null || nominatorPrimeFactors.Count == 0) {
            throw new ArgumentNullException(
                $"{nameof(nominatorPrimeFactors)} must be a non-null, non-empty list");
        }
        if (denominatorPrimeFactors == null || denominatorPrimeFactors.Count == 0) {
            throw new ArgumentNullException(
                $"{nameof(denominatorPrimeFactors)} must be a non-null, non-empty list");
        }
        _nominator = null;
        _denominator = null;
        _nominatorPrimeFactors = nominatorPrimeFactors;
        _denominatorPrimeFactors = denominatorPrimeFactors;
    }

    private int? _nominator;
    public int Nominator
    {
        get {
            if (_nominator == null) {
                _nominator = _nominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
            }
            return _nominator.Value;
        }
    }

    private int? _denominator;
    public int Denominator
    {
        get {
            if (_denominator == null) {
                _denominator = _denominatorPrimeFactors.Aggregate(1, (x, y) => x * y);
            }
            return _denominator.Value;
        }
    }

    private IList<int> _nominatorPrimeFactors;
    public IList<int> NominatorPrimeFactors
    {
        get {
            if (_nominatorPrimeFactors == null) {
                _nominatorPrimeFactors = Factorize(Nominator);
            }
            return _nominatorPrimeFactors;
        }
    }

    private IList<int> _denominatorPrimeFactors;
    public IList<int> DenominatorPrimeFactors
    {
        get {
            if (_denominatorPrimeFactors == null) {
                _denominatorPrimeFactors = Factorize(Denominator);
            }
            return _denominatorPrimeFactors;
        }
    }

    private static List<int> Factorize(int number)
    {
        var result = new List<int>();

        while (number % 2 == 0) {
            result.Add(2);
            number /= 2;
        }

        int factor = 3;
        while (factor * factor <= number) {
            if (number % factor == 0) {
                result.Add(factor);
                number /= factor;
            } else {
                factor += 2;
            }
        }
        if (number > 1) result.Add(number);

        return result;
    }

    public override string ToString()
    {
        if (_nominatorPrimeFactors == null && _denominatorPrimeFactors == null) {
            return $"{_nominator}/{_denominator}";
        }
        string npf = ListToString(_nominatorPrimeFactors);
        string dpf = ListToString(_denominatorPrimeFactors);

        if (_nominator == null && _denominator == null) {
            return $"({npf}) / ({dpf})";
        }
        return $"{_nominator}/{_denominator}, ({npf}) / ({dpf})";


        static string ListToString(IList<int> primeFactors)
        {
            if (primeFactors == null) {
                return null;
            }
            return String.Join(" * ", primeFactors.Select(i => i.ToString()));
        }
    }
}

请注意,声明质数因子会列出IList&lt;int&gt; 允许您使用int[]List&lt;int&gt; 来初始化分数。

但值得考虑的是是否真的需要存储质因数。在某些计算需要时计算它们还不够吗?

【讨论】:

  • 神圣!感谢您的详尽回答。我在我的问题中添加了一些信息。与此同时,我设法拼凑出一个非常不稳定的解决方案:在需要时从Fraction 动态转换为BasicFraction,反之亦然。它有点工作,但我必须为 3 个类定义运算符重载(其中许多类的计算相同,只是类名不同)。现在我正在痛苦地寻找许多错误,因为所有计算和转换中的小错别字。 (仍然要学习如何在 C# 中使用调试器和单元测试)
  • 素数分解主要用于处理像2^(-2/5) 这样的数字,所以它不只是吐出十进制数字。因此,如果我即时计算它,我会不会失去它或最终得到可笑的十进制小数近似值?
  • 当您将十进制数转换为分数时,您必须引入一个精度限制。转换PI的结果应该是什么?使用 0.001 的精度,您最终可能会得到 22/7。如果没有这样的限制,您最终可能会除以 2 个大整数,这是荒谬的。即使使用 1/7 的十进制表示,这也可能导致无意义的结果,因为无穷无尽的小数会在某些地方被截断并引入错误。
  • int/int 和素数表示形式之间的转换结果是否存储或即时计算在精度上没有区别,与小数和小数之间的转换无关。
  • 虽然你是正确的,可区分联合不是 C# 的 语言特性,但它们是 CLR 的 框架特性,并且可以实现从 C# 工作;在我看来,这样做是一个坏主意,并且应该仅用于与使用可区分联合的非托管代码的互操作性。创建联合的方法是使用 FieldOffsetAttribute 告诉 CLR 两个结构字段相互重叠。
【解决方案2】:

你想要一个有区别的联合类型。 C# 本身并不支持它们作为一流的语言功能,但您可以使用 OneOf 破解它:https://github.com/mcintyre321/OneOf

所以你最终会得到:

public interface IFraction
{
  public OneOf< int, Primefactoriztion > a; // nominator
  public OneOf< int, Primefactoriztion > b; // denominator
}

但它仍然是一种代码气味。我认为你需要重新考虑你的整个方法。此外,使用接口来表示 可能不是一个好主意(由于所有堆分配),尤其是因为您的 ab 成员似乎是可变的。


一个更好的主意是更改您的Primefactoriztion 类型(我希望并假设它是struct,而不是class)以支持从int 的隐式转换和一些用于显式转换为int 的成员如果操作不安全(例如,如果值不是整数):

struct Primefactoriztion
{
    public static implicit operator int(Primefactoriztion self)
    {
        return ...
    }
}

这样您就可以安全地消除OneOf&lt;int 并只使用Primefactoriztion

public interface IFraction
{
  public Primefactoriztion a; // nominator
  public Primefactoriztion b; // denominator
}

【讨论】:

  • 嗯,PrimeFactorization 是类,因为我不知道classstruct 之间的区别(从JS 我只知道类的概念)。它将因子存储在 Dictionary&lt;int, BasicFraction&gt; 中(同样,因为 JS 对象)。我只使用BasicFraction 来防止递归(Fraction 将创建PrimeFactorization,这将创建Fraction...)。
猜你喜欢
  • 1970-01-01
  • 2012-09-12
  • 1970-01-01
  • 2013-11-18
  • 2017-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-27
相关资源
最近更新 更多