编码样式问题:我们是否应该具有接受参数,修改参数然后返回该参数的函数?


19

关于这两种做法仅仅是同一枚硬币的两个方面,还是一个真的更好,我正在和我的朋友进行辩论。

我们有一个函数,该函数需要一个参数,填写其中的一个成员,然后返回它:

Item predictPrice(Item item)

我相信,由于它可以处理传入的同一对象,因此没有必要继续返回该项目。实际上,从调用者的角度来看,如果有任何事情,它将使您感到困惑,因为您可以期望它返回一个项目,而不会。

他声称这没有什么区别,即使它确实创建了一个新Item并返回它也没有关系。我强烈不同意,原因如下:

  • 如果您对传入的项有多个引用(或使用指针或其他方式),则分配一个新对象并返回该对象具有重要意义,因为这些引用将是不正确的。

  • 在非内存托管语言中,分配新实例的函数声明了内存的所有权,因此我们将必须实现在某个时候调用的清理方法。

  • 在堆上分配可能很昂贵,因此,对于被调用函数是否执行此操作很重要。

因此,我认为能够通过方法签名查看是修改对象还是分配新对象非常重要。结果,我认为由于函数仅修改传入的对象,因此签名应为:

void predictPrice(Item item)

在我使用过的每个代码库中(公认的C和C ++代码库,而不是Java,这都是我们正在使用的语言),上述样式基本上得到了遵守,并且由经验更为丰富的程序员所遵循。他声称,由于我的代码库和同事的样本量在所有可能的代码库和同事中所占的比例很小,因此,我的经验不能真正表明一个人是否优越。

那么,有什么想法吗?


strcat修改参数并仍然返回它。strcat是否应该返回void?
杰里·耶利米

Java和C ++在传递参数方面有很大不同的行为。如果Itemclass Item ...和不是typedef ...& Item,当地item已经是一个副本
Caleth

Answers:


26

这确实是一个见解,但是出于价值考虑,如果对项目进行了原地修改,我发现返回修改后的项目会产生误导。另外,如果predictPrice要修改该项目,则应使用一个名称来表示要进行此操作(类似setPredictPrice或类似的名称)。

我希望(按顺序)

  1. predictPrice是一种方法Item(没有模作为一个很好的理由,在您的总体设计中很可能是这样),

    • 返回预测价格,或

    • 有一个名字一样setPredictedPrice,而不是

  2. predictPrice 没有修改Item,但是返回了预期的价格

  3. predictPrice是一种void叫做setPredictedPrice

  4. predictPricethis(对于方法链接到其他的方法对什么情况下它的一部分)(和被称为setPredictedPrice


1
谢谢回复。关于1,我们无法在Item内拥有预报价格。2是个好建议-我认为这可能是正确的解决方案。
Squimmy 2014年

2
@Squimmy:果然,我只用了15分钟处理因利用别人的错误正是你是在反对的格局:a = doSomethingWith(b) 修改 b并与修改,后来炸毁了我的代码上认为它知道它回来b了在里面。:-)
TJ Crowder 2014年

11

在面向对象的设计范式中,不应在对象本身之外修改对象。对对象状态的任何更改都应通过对象上的方法来完成。

因此,void predictPrice(Item item)作为其他类的成员函数是错误的。在C时代可能是可以接受的,但是对于Java和C ++,对对象的修改意味着与对象的更深层次的耦合,这很可能会导致其他设计问题(当您重构类并更改其字段时,现在需要将“ predictPrice”更改为其他文件。

返回一个新对象,没有相关的副作用,传入的参数不会更改。您(predictPrice方法)不知道在哪里使用该参数。是Item一个哈希某处的钥匙吗?您这样做是否更改了其哈希码?是否有人坚持要求它不会改变?

这些设计问题强烈建议您不要修改对象(在许多情况下,我会主张不变性),如果这样做,状态的更改应由对象本身而不是某些东西控制和包含。其他在课堂之外。


让我们看一下如果弄乱了哈希中某些字段的情况会发生什么。让我们看一些代码:

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

乙二酮

而且我承认这不是最好的代码(直接访问字段),但是它的目的是证明可变数据被用作HashMap的键,或者在这种情况下,将其放入HashSet。

此代码的输出是:

true
false

发生的事情是,插入哈希码时使用的是哈希对象中的位置。更改用于计算hashCode的值不会重新计算哈希本身。这有将任何可变对象作为哈希键的危险。

因此,回到问题的原始方面,被调用的方法并不“知道”作为参数使用的对象。提供“让对象变异对象”作为执行此操作的唯一方法意味着存在许多细微的错误,它们可能会潜入。就像在哈希中丢失值一样……除非您添加更多并且哈希被重新哈希化,请相信我,那真是个令人讨厌的错误(我失去了价值,直到我向hashMap再添加20个项目,然后突然又出现了)。

  • 除非有非常充分的理由修改的对象,返回一个新的对象是最安全的做法。
  • 当有一个很好的理由修改的对象,该修改应该由对象本身(调用方法),而不是通过可玩弄其领域一些外部函数来完成。
    • 这允许以较低的维护成本重构对象。
    • 这使得对象,以确保该计算其哈希码的值不会改变(或不是其哈希码计算的一部分)

相关:覆盖并返回用作if语句条件的参数的值,在同一if语句内


3
听起来不对。很可能predictPrice不应该直接摆弄Item的成员,但是Item这样做的调用方法有什么问题呢?如果这是唯一要修改的对象,则可以提出一个论点,说它应该是要修改的对象的方法,但是有时修改多个对象(绝对不应该属于同一类),尤其是在相当高级的方法。

@delnan我将承认这可能是对我曾经不得不追踪一次哈希值消失并重新出现的错误的(过度)反应-有人在插入哈希值后对哈希对象中的字段进行了摆弄而不是复制对象以供使用。有一些防御性的编程措施(例如,不可变的对象)可以采取,但是诸如当您不是对象的“所有者”时,重新计算对象本身中的值之类的事情,或者对象本身也不是很容易折回的东西你辛苦了。

您知道,有一个很好的论据,就是使用更多的免费函数而不是类函数来增强封装。自然地,获取常量对象(无论是隐式的此对象还是参数)的函数都不应以任何可观察的方式对其进行更改(某些常量对象可能会缓存事物)。
Deduplicator

2

我始终遵循不变异别人对象的做法。就是说,只对您在其中的class / struct / whathaveyou拥有的对象进行突变。

给定以下数据类:

class Item {
    public double price = 0;
}

还行吧:

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

这很清楚:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

但是,就我的口味而言,这实在是太糊涂和神秘了:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price

请在任何
不赞成投票

1

语法在不同的语言之间是不同的,并且显示一段不特定于某种语言的代码是没有意义的,因为它在不同的语言中意味着完全不同的含义。

在Java中,Item是引用类型,它是指向作为实例的对象的指针的类型Item。在C ++中,这种类型写为Item *(不同语言的同一事物的语法不同)。因此Item predictPrice(Item item)在Java中等同于Item *predictPrice(Item *item)在C ++中。在这两种情况下,它都是一个函数(或方法),该函数接受指向对象的指针,然后返回指向对象的指针。

在C ++中,Item(没有其他任何东西)是对象类型(假设Item是类的名称)。此类型的值“是”一个对象,并Item predictPrice(Item item)声明一个接受对象并返回对象的函数。由于&未使用,因此按值传递和返回。这意味着对象在传递时被复制,在返回时被复制。Java没有等效项,因为Java没有对象类型。

那是什么呢?您在问题中提出的许多问题(例如,“我相信它在传递给...的同一对象上起作用”)取决于准确地了解此处传递和返回的内容。


1

我看到的代码库所遵循的约定是希望从语法中可以清楚地看出一种样式。如果函数更改值,则首选通过指针传递

void predictPrice(Item * const item);

这种风格导致:

  • 调用它,您将看到:

    预报价格(&my_item);

并且您知道只要坚持样式,它就会被修改。

  • 否则,当您不希望函数修改实例时,最好通过const ref或value传递。

应该注意的是,即使这是更清晰的语法,在Java中也不可用。
Ben Leggiero

0

尽管我没有强烈的偏好,但我还是投票赞成返回对象可以为API提供更好的灵活性。

注意,只有在该方法具有返回其他对象的自由时,它才有意义。如果您的javadoc说的@returns the same value of parameter a话那就没用了。但是,如果您的javadoc说@return an instance that holds the same data than parameter a,则返回相同的实例或另一个实例是实现细节。

例如,在我当前的项目(Java EE)中,我有一个业务层。在数据库中存储一个雇员(并分配一个自动ID),例如,我有一个雇员

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

当然,此实现没有区别,因为JPA在分配ID之后返回相同的对象。现在,如果我切换到JDBC

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

现在,在????? 我有三个选择。

  1. 为Id定义一个setter,然后我将返回相同的对象。丑陋,因为setId将无处不在。

  2. 做一些沉重的反射工作,并在Employee对象中设置id 。肮脏,但至少仅限于createEmployee记录。

  3. 提供一个构造函数,该构造函数可以复制原始文件Employee并接受idto设置。

  4. 从数据库中检索对象(如果在数据库中计算了某些字段值,或者希望填充房地产,则可能需要)。

现在,对于1.和2.,您可能要返回相同的实例,但是对于3.和4.,您将要返回新的实例。如果从原则上您在API中坚持认为“真实”值将是该方法返回的值,那么您将拥有更大的自由度。

我唯一可以反对的说法是,使用实施1或2,使用您的API的人可能会忽略结果的分配,如果您更改为3或4的实现,他们的代码就会中断。

无论如何,正如您所看到的,我的偏好是基于其他个人偏好(例如,禁止使用IDS的二传手),因此也许它并不适用于所有人。


0

用Java进行编码时,应尝试使可变类型的每个对象的用法与以下两种模式之一匹配:

  1. 确切地,一个实体被视为可变对象的“所有者”,并将该对象的状态视为其自身的一部分。其他实体可能拥有引用,但应将引用视为标识其他人拥有的对象。

  2. 尽管对象是可变的,但没有任何持有该对象的引用被允许对其进行突变,从而使该实例有效地不可变(请注意,由于Java内存模型中的古怪之处,没有使有效不可变的对象具有线程的好方法,安全的不可变语义,而无需为每次访问添加额外的间接级别)。任何使用对此类对象的引用封装状态并想要更改封装状态的实体都必须创建一个包含适当状态的新对象。

我不喜欢让方法使基础对象发生变化并返回引用,因为它们看起来很适合上面的第二种模式。在某些情况下,该方法可能会有所帮助,但是将要使对象发生突变的代码应该看起来像是要这样做。像这样的代码myThing = myThing.xyz(q);看起来myThing比简单地修改它标识的对象要少得多myThing.xyz(q);

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.