【问题标题】:API Design for Idiot-Proof Iteration Without Generics没有泛型的白痴迭代的 API 设计
【发布时间】:2011-09-15 16:14:34
【问题描述】:

当您为代码库设计 API 时,您希望它易于使用且难以使用。理想情况下,您希望它是白痴证明。

您可能还希望使其与无法处理泛型的旧系统兼容,例如 .Net 1.1 和 Java 1.4。但是您不希望从较新的代码中使用它会很痛苦。

我想知道以类型安全的方式使事物易于迭代的最佳方法...记住您不能使用泛型,因此 Java 的 Iterable<T> 和 .Net 的 IEnumerable<T> 已经过时了。

您希望人们能够在 Java (for Item i : items) 中使用增强的 for 循环,以及在 .Net 中使用 foreach / For Each 循环,并且您不希望他们必须进行任何转换。基本上,您希望您的 API 现在友好且向后兼容。

我能想到的最好的类型安全选项是数组。它们完全向后兼容并且它们很容易以类型安全的方式进行迭代。但是数组并不理想,因为你不能让它们不可变。因此,当您有一个包含数组的不可变对象时,您希望人们能够对其进行迭代,为了保持不变性,您必须在他们每次访问它时提供一个防御性副本。

在 Java 中,执行 (MyObject[]) myInternalArray.clone(); 非常快。我确信 .Net 中的等价物也非常快。如果你喜欢:

class Schedule {
   private Appointment[] internalArray;
   public Appointment[] appointments() {
       return (Appointment[]) internalArray.clone();
   }
}

人们可以这样做:

for (Appointment a : schedule.appointments()) {
    a.doSomething();
}

它将简单、清晰、类型安全且快速。

但是他们可以做这样的事情:

for (int i = 0; i < schedule.appointments().length; i++) {
    Appointment a = schedule.appointments()[i];
}

然后它会非常低效,因为每次迭代都会克隆整个约会数组(一次用于长度测试,一次用于获取索引处的对象)。如果数组很小,则不是这样的问题,但如果数组中有数千个项目,则非常可怕。呵呵。

真的有人会这样做吗?我不确定...我想这主要是我的问题。

您可以调用方法toAppointmentArray() 而不是appointments(),这可能会降低任何人以错误方式使用它的可能性。但这也会让人们更难找到他们只想迭代约会的时间。

你当然会清楚地记录appointments(),说它返回一个防御性副本。但是很多人不会阅读那些特定的文档。

虽然我欢迎提出建议,但在我看来,没有完美的方法可以让它简单、清晰、类型安全、白痴证明。如果 少数 人在不知情的情况下克隆数组数千次,我是否失败了,或者对于 大多数 的简单、类型安全的迭代来说,这是可以接受的代价?


NB 我碰巧正在为 Java 和 .Net 设计这个库,这就是为什么我试图让这个问题适用于两者。我将其标记为与语言无关,因为这也是其他语言也可能出现的问题。代码示例使用 Java,但 C# 类似(尽管可以选择将 Appointments 访问器设为属性)。


更新:我做了一些快速的性能测试,看看这对 Java 有多大的影响。我测试过:

  1. 克隆一次数组,然后使用增强的 for 循环对其进行迭代
  2. 使用 ArrayList 迭代 增强的 for 循环
  3. 迭代不可修改的 数组列表(来自 Collections.unmodifyableList) 使用 增强的 for 循环
  4. 以错误的方式迭代数组(在长度检查中重复克隆它 以及获取每个索引项时)。

对于 10 个对象,相对速度(多次重复并取中位数)如下:

  1. 1,000
  2. 1,300
  3. 1,300
  4. 5,000

对于 100 个对象:

  1. 1,300
  2. 4,900
  3. 6,300
  4. 85,500

对于 1000 个对象:

  1. 6,400
  2. 51,700
  3. 56,200
  4. 7,000,300

对于 10000 个对象:

  1. 68,000
  2. 445,000
  3. 651,000
  4. 655,180,000

肯定是粗略的数字,但足以让我相信两件事:

【问题讨论】:

  • 永远不要低估白痴的力量......
  • @Marc 这就是我的态度。在迎合它时很难弄清楚在哪里划清界限,如果迎合它会让其他人的事情变得更难。
  • 实际上大多数白痴都有足够的意识将appointments()的结果缓存在局部变量中。其中一些甚至会缓存[].length。即使是白痴也很有防御性。

标签: java .net api language-agnostic api-design


【解决方案1】:

clone() 很快,但不是我所说的超级快。

如果你不相信人们能有效地编写循环,我不会让他们编写循环(这也避免了对 clone() 的需要)

interface AppointmentHandler {
    public void onAppointment(Appointment appointment);
}

class Schedule {
    public void forEachAppointment(AppointmentHandler ah) {
        for(Appointment a: internalArray)
            ah.onAppointment(a);
    }
}

【讨论】:

  • 这是一个不错的模式,但缺点是我认为它会使一些人感到困惑。我更新了我的帖子以显示一些测试的结果,这些测试表明克隆然后迭代在性能方面是可以的。但是你说得对,克隆并不是真的超级快,因为如果是这样,那么在循环中重复克隆就没有问题了。测试清楚地表明,这会产生可怕的结果。
【解决方案2】:

由于您不能同时拥有这两种方式,我建议您创建 API 的预泛型和泛型版本。理想情况下,底层实现可以大体相同,但事实是,如果您希望任何使用 Java 1.5 或更高版本的人都能轻松使用它,他们会期望使用泛型和可迭代以及所有更新的语言特性。

我认为数组的使用应该是不存在的。在这两种情况下,它都不能提供易于使用的 API。

注意:我从未使用过 C#,但我希望同样如此。

【讨论】:

  • 这是一种诱人的方法——我很可能会效仿。令人沮丧的是,我对几乎所有 API 都有一个很好的非泛型设计,除了一些包含列表或数组并且人们需要能够迭代的对象。
【解决方案3】:

就少数用户而言,那些在循环的每次迭代中调用相同方法来获取相同对象的用户将要求效率低下,而不管 API 设计如何。我认为只要有据可查,要求用户遵守一些常识并不过分。

【讨论】:

    猜你喜欢
    • 2010-12-31
    • 1970-01-01
    • 2023-04-08
    • 2010-09-05
    • 2023-02-01
    • 2018-12-09
    • 1970-01-01
    • 2019-07-19
    • 1970-01-01
    相关资源
    最近更新 更多