dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.2k stars 1.57k forks source link

Add clone() method to Object class to create an independent copy instance of any object #45033

Open enloc-port opened 3 years ago

enloc-port commented 3 years ago

I know this has been asked multiple times already (#3367, #35309), but since this is not just about simple classes, but also about methods, and developers run into this issue quite often, I wanted to elaborate on this a little bit.

Creating independent copies of objects can be somewhat cumbersome, because objects (except for literals) are usually passed by reference.

class Foo {
  Foo(this.bars);
  final List<Bar> bars;
}

class Bar {
  Bar(this.strings);
  final List<String> strings;
}

void alter(List<Bar> bars) {
  bars[0].strings.insert(1, 'good');
  bars[1].strings.insert(1, 'fine');
}

void main() {
  final Bar bar1 = Bar('hello world'.split(' '));
  final Bar bar2 = Bar(<String>[...bar1.strings]);

  final Foo foo1 = Foo(<Bar>[bar1, bar2]);
  final Foo foo2 = Foo(<Bar>[...foo1.bars]);

  alter([...foo1.bars]);

  print(bar1.strings); // [hello, good, world]
  print(bar2.strings); // [hello, fine, world]
  print(foo1.bars[0].strings); // [hello, good, world]
  print(foo1.bars[1].strings); // [hello, fine, world]
  print(foo2.bars[0].strings); // [hello, good, world] -> not cool
  print(foo2.bars[1].strings); // [hello, fine, world] -> not cool
}

I don't want to create custom classes extending basic classes like List or Map, and then create a dedicated clone() method for/in every single one of the classes I use and create myself.

I also don't want to use JsonSerializable and Foo.fromJson(jsonDecode(jsonEncode(obj))) everywhere, since that a is a rediculessly heavy load, especially for large objects, just to create a copy. Plus, I don't even know if this works flawlessly with rather complex objects.

Shouldn't it be possible to add a clone() method to the Object class, which creates an independent instance of that object, without properties referencing each other?

// uses classes [Foo] and [Bar] and [alter] function from code snippet above
void suggestion() {
  final Bar bar1 = Bar('hello world'.split(' '));
  final Bar bar2 = Bar([...bar1.strings]);

  final Foo foo1 = Foo(<Bar>[bar1, bar2]);

  // create a new List<Bar> with independent instances from [bar1] and [bar2]
  final Foo foo2 = Foo(foo1.bars.map((Bar bar) => bar.clone()));

  // or even better:
  // final Foo foo2 = Foo(foo1.bars.clone());

  // At this point, the objects in [foo2.bars] shall be independent from those in [foo1.bars]

  alter([...foo1.bars]);

  print(bar1.strings); // [hello, good, world]
  print(bar2.strings); // [hello, fine, world]
  print(foo1.bars[0].strings); // [hello, good, world]
  print(foo1.bars[1].strings); // [hello, fine, world]
  print(foo2.bars[0].strings); // [hello, world] -> because cloned, not referenced
  print(foo2.bars[1].strings); // [hello, world] -> because cloned, not referenced
}

I am a Flutter developer, so I don't know how deeply into the OS and on the device Dart can go, whether it is able or not to work on the RAM as languages like C or C++ can. But I figured, a low level copy on the RAM might actually be the most efficient way, rather than trying some complicated recursive way on the "Dart language" layer.

mkustermann commented 3 years ago

/cc @lrhn

simolus3 commented 3 years ago

Not great, but the isolate library can be misused to implement this:

import 'dart:isolate';

extension Clone<T> on T {
  Future<T> clone() {
    final receive = ReceivePort();
    receive.sendPort.send(this);

    return receive.first.then((e) => e as T).whenComplete(receive.close);
  }
}

If we end up having blocking receive ports (#44804), this wouldn't have to be async either.

lrhn commented 3 years ago

It's not something we generally want (and the isolate approach is something I'm not too happy with to begin with).

Some classes can have invariants which are not entirely contained inside the object. Perhaps a class registers all new instances created in a global registry, or with an expando. The cloned object fails this invariant. The class would have to be aware of this and override clone to also register the new instance.

Some classes might be depending on a private component not being shared with anyone. If the cloning is shallow, that will stop being the case. Shallow-cloning a growable list would create two growable lists sharing the same backing store. Again, the class would have to be aware of this and override clone to avoid the problem.

Similarly, some classes may be identity based, and doing a deep clone would be bad for them. A deep clone of an identity-based hash set would change the hash codes of the cloned elements, but not the integer hash codes already stored in the data-structure, making look-up fail. And again, the class would have to be aware of it and override clone.

Too many things could go wring if we just added clone on all objects. Existing classes would need to be updated to support it properly, and if they don't, it'll be unsafe to use the functionality.

That suggests that we should instead make it an opt-in operation.

In Java, you have clone on Object, but the class has to implement Cloneable to actually be able to use it. You can't just clone any object. They also have cloning-by-serialization issues, up to and including security bugs, but at least have ways to interact with the serialization when necessary. The big problem is that you have to remember doing so.

Dart could also add a Cloneable interface, we just need a way to get the default implementation of "bitwise copy" for classes which don't want something else. Maybe if we had interface default methods, the Cloneable interface could inherit that if you don't write something else.

NicolasCharpentier commented 3 years ago

Adding my junior-to-Dart point of view, I am wondering first why do people need to clone objects, and maybe it's the same reason for 90% of cases. I imagine that Dart is used mainly in Flutter, which is a declarative UI library, as React. With that comes states managements tools that makes your widgets reload when their picked data changes.

So you may see where I'm coming, maybe the real solution to the issue is to have another type of classes (data classes, structs), which could hold only other data classes / primitives, and nether have logic (?). The clone operation would be possible I guess. I know there must be issues on that data classes thing.

So my point is : Why do people want to clone stuff ? For me, it's to "update" anemic objects, and not rich models or any other class.

saidani300 commented 3 years ago

Adding my junior-to-Dart point of view, I am wondering first why do people need to clone objects, and maybe it's the same reason for 90% of cases. I imagine that Dart is used mainly in Flutter, which is a declarative UI library, as React. With that comes states managements tools that makes your widgets reload when their picked data changes.

So you may see where I'm coming, maybe the real solution to the issue is to have another type of classes (data classes, structs), which could hold only other data classes / primitives, and nether have logic (?). The clone operation would be possible I guess. I know there must be issues on that data classes thing.

So my point is : Why do people want to clone stuff ? For me, it's to "update" anemic objects, and not rich models or any other class.

Maybe to filter a list of objects without losing the initial instance? If you have a better solution for that without creating a copy of an object, let me know. Thank you in advance.

lrhn commented 3 years ago

Cloning a list is trivial, just do var newList = list.toList();. All properties of List can be introspected (well, except the type parameter). Also, the List constructor is trivial, it does nothing except initialize the publicly visible properties of the object.

Cloning a more complicated object with private fields and global state depending on the object is a much different thing. If an object does not provide you with a way to clone it, and it doesn't provide you with all the information you need to create a another object with the same state, that's quite possibly deliberate. The private state of that object is not yours to play with, and creating objects without going through a constructor is not something the class is written to support.

Allowing you to clone such an object anyway, from the outside, risks creating an object that doesn't actually work (cloning an identity based HashMap is one example, cloning an object which requires being registered in an Expando is another). Obviously they can just override Object.clone to throw or return a properly constructed object, or the same object if that's what they want, but then the entire idea goes away.

So, in short, if you want to clone an object, and that object and it's class doesn't give you a way to do so, maybe you shouldn't be doing it.

HeyAvi commented 2 years ago

Adding my junior-to-Dart point of view, I am wondering first why do people need to clone objects, and maybe it's the same reason for 90% of cases. I imagine that Dart is used mainly in Flutter, which is a declarative UI library, as React. With that comes states managements tools that makes your widgets reload when their picked data changes.

So you may see where I'm coming, maybe the real solution to the issue is to have another type of classes (data classes, structs), which could hold only other data classes / primitives, and nether have logic (?). The clone operation would be possible I guess. I know there must be issues on that data classes thing.

So my point is : Why do people want to clone stuff ? For me, it's to "update" anemic objects, and not rich models or any other class.

Cause sometimes backend developers make data structures as s**t and for that we need to do so many stupid things. And when we ask to change the structure they won't they'll ask you to manage

KiraResari commented 1 year ago

My reason for cloning an object is that I want to have an edit screen where I can edit the fields of an object, and that edit screen should only display the "Save" and "Reset" buttons if the Object differs from the original object. For that reason, I'll have some logic along the lines of:

var originalObject = myObject.copy();
[...]
changeObjectField(String input){
    myObject.field=input;
}
[...]
displayButtonsIfObjectChanged(){
    if(myObject != originalObject){
        displayButtons();
    }
}

In this case, the object naturally has an equals method that evaluates to true if all the fields have equal values.

This has the result that the buttons appear as soon as a field is changed, and disappear again if you manually change them back to the original value. The extended application for that is naturally that a file can display when it has changed and needs saving on a "by content" basis, as opposed to simply setting a flag as soon as the first change is made.

TouseefAQ commented 10 months ago

You can get help from the compute function Not Recommended though.

final clonedData = await compute((dynamic data) => return data));

lrhn commented 10 months ago

That's effectively sending the object through an isolate communication port. You don't need an extra isolate for it, you can send and receive in the same isolate:

import "dart:isolate";
Future<T> clone<T>(T value) async {
  var port = ReceivePort();
  port.sendPort.send(value);
  return (await port.first) as T;
}

But I really do not recommend doing it, because

  1. It's expensive.
  2. Most objects are not intended to be cloned. Those that are will already provide functionality for you to do so, fx a copy constructor.

Any number of things can go wrong when copying objects. If the object has relations to static data around it, that state might not recognize the new copy. (If the object is entirely self-contained, then it's likely a data object, and it'll likely already have copy functionality. Or be immutable, so you shouldn't need to copy.)

You may end up with two objects with identical "unique" IDs. Or with objects containing hash tables of other objects that don't have the same identity hash code as when they were inserted. Or objects which are not registered in a registry or Expando that all objects of that type are assumed to be in.

And if you do it anyway, and things crash, remember that you asked for it :) And if you do it, and your program crashes, for any other reason, you will always have the nagging thought that maybe it's because you copied objects. :stuck_out_tongue:

makoConstruct commented 4 months ago

I think the Cloneable interface that lhrn suggested is the correct approach. You might need #3025: Self types though?

abstract class Cloneable {
  Self clone();
}
lrhn commented 4 months ago

You can get far with F-bounded polymorphism:

abstract interface class Clonable<T extends Clonable<T>> {
  T clone();
}

Then you can have:

class Point implements Clonable<Point> {
  final double x, y;
  Point(this.x, this.y);

  Point rotate(double by) { 
    var s = sin(by);
    var c = cos(by);
    return Point(x * c - y * s, x * s + y * c);
  }
  Point clone() => Point(x, y);
  String toString() => "${(x, y)}";
}

and a private helper class that is a Point, but allows other operations to be done more efficiently.

class _PolarPoint implements Point {
  double _arg, _mag;
  factory _PolarPoint(double arg, double magnitude) : _arg = arg, _mag = magnitude;

  // Cheap rotate.
  Point rotate(double by) => _PolarPoint((_arg + by) % (pi * 2), _mag);

  // Expensive Cartesian coordinates.
  double get x => cos(_arg) * _mag;
  double get y => sin(_arg) * _mag;

  Point clone() => Point(x, y); // Valid clone of Clonable<Point>.
  String toString() => "${(x, y)}";
}

If you don't use self-types, then the type of clone can be a supertype (well, or any other type for that matter, but that gets weird fast). Using a supertype makes sense for private subtypes used internally by a library, that may want to be something else if cloned.

You don't even need to F-bounded polymorphism, it can just be Clonable<T>, but then you don't know that you can clone the result again. The F-bounded T extends<Clonable<T>> encourages things to be clonable to themselves, or at least something else that is itself clonable.