Skip to content

Commit d17fa16

Browse files
authored
Override == and hashCode in PbMap (#224)
1 parent 532bfc9 commit d17fa16

12 files changed

+129
-46
lines changed

protobuf/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.13.7
2+
3+
* Override `operator ==` and `hashCode` in `PbMap` so that two `PbMap`s are equal if they have equal key/value pairs.
4+
15
## 0.13.6
26

37
* Fixed equality check between messages with and without extensions.

protobuf/lib/src/protobuf/coded_buffer.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ void _writeToCodedBufferWriter(_FieldSet fs, CodedBufferWriter out) {
1616
}
1717

1818
if (fs._hasExtensions) {
19-
for (var tagNumber in sorted(fs._extensions._tagNumbers)) {
19+
for (var tagNumber in _sorted(fs._extensions._tagNumbers)) {
2020
var fi = fs._extensions._getInfoOrNull(tagNumber);
2121
out.writeField(tagNumber, fi.type, fs._extensions._getFieldOrNull(fi));
2222
}

protobuf/lib/src/protobuf/field_set.dart

+25-28
Original file line numberDiff line numberDiff line change
@@ -523,51 +523,48 @@ class _FieldSet {
523523
/// The hash may change when any field changes (recursively).
524524
/// Therefore, protobufs used as map keys shouldn't be changed.
525525
int get _hashCode {
526-
int hash;
527-
528-
void hashEnumList(PbListBase enums) {
529-
for (ProtobufEnum enm in enums) {
530-
hash = 0x1fffffff & ((31 * hash) + enm.value);
531-
}
532-
}
533-
534526
// Hashes the value of one field (recursively).
535-
void hashField(FieldInfo fi, value) {
527+
int hashField(int hash, FieldInfo fi, value) {
536528
if (value is List && value.isEmpty) {
537-
return; // It's either repeated or an empty byte array.
529+
return hash; // It's either repeated or an empty byte array.
538530
}
539-
hash = 0x1fffffff & ((37 * hash) + fi.tagNumber);
531+
532+
hash = _HashUtils._combine(hash, fi.tagNumber);
540533
if (!_isEnum(fi.type)) {
541-
hash = 0x1fffffff & ((53 * hash) + value.hashCode);
534+
hash = _HashUtils._combine(hash, value.hashCode);
542535
} else if (fi.isRepeated) {
543-
hashEnumList(value);
536+
hash = _HashUtils._hashObjects(value.map((enm) => enm.value));
544537
} else {
545538
ProtobufEnum enm = value;
546-
hash = 0x1fffffff & ((53 * hash) + enm.value);
539+
hash = _HashUtils._combine(hash, enm.value);
547540
}
541+
542+
return hash;
548543
}
549544

550-
void hashEachField() {
551-
for (var fi in _infosSortedByTag) {
552-
var v = _values[fi.index];
553-
if (v != null) hashField(fi, v);
554-
}
555-
if (!_hasExtensions) return;
556-
for (int tagNumber in sorted(_extensions._tagNumbers)) {
545+
int hashEachField(int hash) {
546+
//non-extension fields
547+
hash = _infosSortedByTag.where((fi) => _values[fi.index] != null).fold(
548+
hash, (int h, FieldInfo fi) => hashField(h, fi, _values[fi.index]));
549+
550+
if (!_hasExtensions) return hash;
551+
552+
hash =
553+
_sorted(_extensions._tagNumbers).fold(hash, (int h, int tagNumber) {
557554
var fi = _extensions._getInfoOrNull(tagNumber);
558-
hashField(fi, _extensions._getFieldOrNull(fi));
559-
}
555+
return hashField(h, fi, _extensions._getFieldOrNull(fi));
556+
});
557+
558+
return hash;
560559
}
561560

562-
// Generate hash.
563-
hash = 41;
564561
// Hash with descriptor.
565-
hash = 0x1fffffff & ((19 * hash) + _meta.hashCode);
562+
int hash = _HashUtils._combine(0, _meta.hashCode);
566563
// Hash with fields.
567-
hashEachField();
564+
hash = hashEachField(hash);
568565
// Hash with unknown fields.
569566
if (_hasUnknownFields) {
570-
hash = 0x1fffffff & ((29 * hash) + _unknownFields.hashCode);
567+
hash = _HashUtils._combine(hash, _unknownFields.hashCode);
571568
}
572569
return hash;
573570
}

protobuf/lib/src/protobuf/json.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Map<String, dynamic> _writeToJsonMap(_FieldSet fs) {
6262
result['${fi.tagNumber}'] = convertToMap(value, fi.type);
6363
}
6464
if (fs._hasExtensions) {
65-
for (int tagNumber in sorted(fs._extensions._tagNumbers)) {
65+
for (int tagNumber in _sorted(fs._extensions._tagNumbers)) {
6666
var value = fs._extensions._values[tagNumber];
6767
if (value is List && value.isEmpty) {
6868
continue; // It's repeated or an empty byte array.

protobuf/lib/src/protobuf/pb_list.dart

+2-11
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,8 @@ abstract class PbListBase<E> extends ListBase<E> {
171171
bool operator ==(other) =>
172172
(other is PbListBase) && _areListsEqual(other, this);
173173

174-
int get hashCode {
175-
int hash = 0;
176-
for (final value in _wrappedList) {
177-
hash = 0x1fffffff & (hash + value.hashCode);
178-
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
179-
hash = hash ^ (hash >> 6);
180-
}
181-
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
182-
hash = hash ^ (hash >> 11);
183-
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
184-
}
174+
@override
175+
int get hashCode => _HashUtils._hashObjects(_wrappedList);
185176

186177
/// Returns an [Iterator] for the list.
187178
Iterator<E> get iterator => _wrappedList.iterator;

protobuf/lib/src/protobuf/pb_map.dart

+34
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,40 @@ class PbMap<K, V> extends MapBase<K, V> {
3939
_wrappedMap[key] = value;
4040
}
4141

42+
/// A [PbMap] is equal to another [PbMap] with equal key/value
43+
/// pairs in any order.
44+
@override
45+
bool operator ==(other) {
46+
if (identical(other, this)) {
47+
return true;
48+
}
49+
if (other is! PbMap) {
50+
return false;
51+
}
52+
if (other.length != length) {
53+
return false;
54+
}
55+
for (final key in keys) {
56+
if (!other.containsKey(key)) {
57+
return false;
58+
}
59+
}
60+
for (final key in keys) {
61+
if (other[key] != this[key]) {
62+
return false;
63+
}
64+
}
65+
return true;
66+
}
67+
68+
/// A [PbMap] is equal to another [PbMap] with equal key/value
69+
/// pairs in any order. Then, the `hashCode` is guaranteed to be the same.
70+
@override
71+
int get hashCode {
72+
return _wrappedMap.entries
73+
.fold(0, (h, entry) => h ^ _HashUtils._hash2(entry.key, entry.value));
74+
}
75+
4276
@override
4377
void clear() {
4478
if (_isReadonly)

protobuf/lib/src/protobuf/unknown_field_set.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class UnknownFieldSet {
151151
String _toString(String indent) {
152152
var stringBuffer = StringBuffer();
153153

154-
for (int tag in sorted(_fields.keys)) {
154+
for (int tag in _sorted(_fields.keys)) {
155155
var field = _fields[tag];
156156
for (var value in field.values) {
157157
if (value is UnknownFieldSet) {

protobuf/lib/src/protobuf/utils.dart

+29-1
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,32 @@ bool _areByteDataEqual(ByteData lhs, ByteData rhs) {
3535
return _areListsEqual(asBytes(lhs), asBytes(rhs));
3636
}
3737

38-
List<T> sorted<T>(Iterable<T> list) => List.from(list)..sort();
38+
@Deprecated("This function was not intended to be public. "
39+
"It will be removed from the public api in next major version. ")
40+
List<T> sorted<T>(Iterable<T> list) => new List.from(list)..sort();
41+
42+
List<T> _sorted<T>(Iterable<T> list) => new List.from(list)..sort();
43+
44+
class _HashUtils {
45+
// Jenkins hash functions copied from https://github.com/google/quiver-dart/blob/master/lib/src/core/hash.dart.
46+
47+
static int _combine(int hash, int value) {
48+
hash = 0x1fffffff & (hash + value);
49+
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
50+
return hash ^ (hash >> 6);
51+
}
52+
53+
static int _finish(int hash) {
54+
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
55+
hash = hash ^ (hash >> 11);
56+
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
57+
}
58+
59+
/// Generates a hash code for multiple [objects].
60+
static int _hashObjects(Iterable objects) =>
61+
_finish(objects.fold(0, (h, i) => _combine(h, i.hashCode)));
62+
63+
/// Generates a hash code for two objects.
64+
static int _hash2(a, b) =>
65+
_finish(_combine(_combine(0, a.hashCode), b.hashCode));
66+
}

protobuf/pubspec.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: protobuf
2-
version: 0.13.6
2+
version: 0.13.7
33
author: Dart Team <misc@dartlang.org>
44
description: >
55
Runtime library for protocol buffers support.

protoc_plugin/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## 16.0.3
2+
23
* Sync Kythe metadata updates from internal repo:
34
* Remove the extra 1 (field name) from generated field paths,
45
so they refer to the whole field rather than the name.

protoc_plugin/pubspec.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: protoc_plugin
2-
version: 16.0.3-dev-1
2+
version: 16.0.3-dev-2
33
author: Dart Team <misc@dartlang.org>
44
description: Protoc compiler plugin to generate Dart code
55
homepage: https://github.com/dart-lang/protobuf
@@ -10,7 +10,7 @@ environment:
1010
dependencies:
1111
fixnum: ^0.10.5
1212
path: ^1.0.0
13-
protobuf: ^0.13.6
13+
protobuf: ^0.13.7
1414
dart_style: ^1.0.6
1515

1616
dev_dependencies:

protoc_plugin/test/map_field_test.dart

+28
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ library map_field_test;
77

88
import 'dart:convert';
99

10+
import 'package:protobuf/protobuf.dart';
1011
import 'package:test/test.dart';
1112

1213
import '../out/protos/map_field.pb.dart';
@@ -212,6 +213,33 @@ void main() {
212213
_expectEmpty(testMap);
213214
});
214215

216+
test(
217+
'PbMap` is equal to another PbMap with equal key/value pairs in any order',
218+
() {
219+
TestMap t = TestMap()
220+
..int32ToStringField[2] = 'test2'
221+
..int32ToStringField[1] = 'test';
222+
TestMap t2 = TestMap()
223+
..int32ToStringField[1] = 'test'
224+
..int32ToStringField[2] = 'test2';
225+
TestMap t3 = TestMap()..int32ToStringField[1] = 'test';
226+
227+
PbMap<int, String> m = t.int32ToStringField;
228+
PbMap<int, String> m2 = t2.int32ToStringField;
229+
PbMap<int, String> m3 = t3.int32ToStringField;
230+
231+
expect(t, t2);
232+
expect(t.hashCode, t2.hashCode);
233+
234+
expect(m, m2);
235+
expect(m == m2, isTrue);
236+
expect(m.hashCode, m2.hashCode);
237+
238+
expect(m, isNot(m3));
239+
expect(m == m3, isFalse);
240+
expect(m.hashCode, isNot(m3.hashCode));
241+
});
242+
215243
test('merge from other message', () {
216244
TestMap testMap = TestMap();
217245
_setValues(testMap);

0 commit comments

Comments
 (0)