-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[pigeon] Optimize data class equality and hashing in Dart, Kotlin, java, and Swift, adds equality in other languages #11140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
82065a8
9861c00
11daedd
e197fc0
cbf4096
935aa47
4e9541f
f30b44c
08a79f3
8cb51ac
ddc41c2
4ad3636
0a08ef5
5855b23
f99e64a
101fb0f
7309d17
24d0f23
21ad393
94a58c8
3fdd987
b920f50
7df481e
25021e7
4cbfdcd
8c75a36
655229c
80d8bbc
a57dd6f
4ded545
8698cb5
7355a96
45d8207
97b8e8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,14 +19,146 @@ | |
| import java.lang.annotation.Target; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| /** Generated class from Pigeon. */ | ||
| @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) | ||
| public class Messages { | ||
| static boolean pigeonDeepEquals(Object a, Object b) { | ||
| if (a == b) { | ||
| return true; | ||
| } | ||
| if (a == null || b == null) { | ||
| return false; | ||
| } | ||
| if (a instanceof byte[] && b instanceof byte[]) { | ||
| return Arrays.equals((byte[]) a, (byte[]) b); | ||
| } | ||
| if (a instanceof int[] && b instanceof int[]) { | ||
| return Arrays.equals((int[]) a, (int[]) b); | ||
| } | ||
| if (a instanceof long[] && b instanceof long[]) { | ||
| return Arrays.equals((long[]) a, (long[]) b); | ||
| } | ||
| if (a instanceof double[] && b instanceof double[]) { | ||
| double[] da = (double[]) a; | ||
| double[] db = (double[]) b; | ||
| if (da.length != db.length) return false; | ||
| for (int i = 0; i < da.length; i++) { | ||
| if (!pigeonDeepEquals(da[i], db[i])) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| if (a instanceof List && b instanceof List) { | ||
| List<?> listA = (List<?>) a; | ||
| List<?> listB = (List<?>) b; | ||
| if (listA.size() != listB.size()) { | ||
| return false; | ||
| } | ||
| for (int i = 0; i < listA.size(); i++) { | ||
| if (!pigeonDeepEquals(listA.get(i), listB.get(i))) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| if (a instanceof Map && b instanceof Map) { | ||
| Map<?, ?> mapA = (Map<?, ?>) a; | ||
| Map<?, ?> mapB = (Map<?, ?>) b; | ||
| if (mapA.size() != mapB.size()) { | ||
| return false; | ||
| } | ||
| for (Map.Entry<?, ?> entryA : mapA.entrySet()) { | ||
| Object keyA = entryA.getKey(); | ||
| Object valueA = entryA.getValue(); | ||
| boolean found = false; | ||
| for (Map.Entry<?, ?> entryB : mapB.entrySet()) { | ||
| Object keyB = entryB.getKey(); | ||
| if (pigeonDeepEquals(keyA, keyB)) { | ||
| Object valueB = entryB.getValue(); | ||
| if (pigeonDeepEquals(valueA, valueB)) { | ||
| found = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (!found) { | ||
| return false; | ||
| } | ||
| } | ||
| return true; | ||
| } | ||
| if (a instanceof Double && b instanceof Double) { | ||
| return ((double) a == 0.0 ? 0.0 : (double) a) == ((double) b == 0.0 ? 0.0 : (double) b) | ||
| || (Double.isNaN((double) a) && Double.isNaN((double) b)); | ||
| } | ||
| if (a instanceof Float && b instanceof Float) { | ||
| return ((float) a == 0.0f ? 0.0f : (float) a) == ((float) b == 0.0f ? 0.0f : (float) b) | ||
| || (Float.isNaN((float) a) && Float.isNaN((float) b)); | ||
| } | ||
| return a.equals(b); | ||
| } | ||
|
|
||
| static int pigeonDeepHashCode(Object value) { | ||
| if (value == null) { | ||
| return 0; | ||
| } | ||
| if (value instanceof byte[]) { | ||
| return Arrays.hashCode((byte[]) value); | ||
| } | ||
| if (value instanceof int[]) { | ||
| return Arrays.hashCode((int[]) value); | ||
| } | ||
| if (value instanceof long[]) { | ||
| return Arrays.hashCode((long[]) value); | ||
| } | ||
| if (value instanceof double[]) { | ||
| double[] da = (double[]) value; | ||
| int result = 1; | ||
| for (double d : da) { | ||
| result = 31 * result + pigeonDeepHashCode(d); | ||
| } | ||
| return result; | ||
| } | ||
| if (value instanceof List) { | ||
| int result = 1; | ||
| for (Object item : (List<?>) value) { | ||
| result = 31 * result + pigeonDeepHashCode(item); | ||
| } | ||
| return result; | ||
| } | ||
| if (value instanceof Map) { | ||
| int result = 0; | ||
| for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) { | ||
| result += (pigeonDeepHashCode(entry.getKey()) ^ pigeonDeepHashCode(entry.getValue())); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why no multiplication on this one? |
||
| } | ||
| return result; | ||
| } | ||
| if (value instanceof Object[]) { | ||
| int result = 1; | ||
| for (Object item : (Object[]) value) { | ||
| result = 31 * result + pigeonDeepHashCode(item); | ||
| } | ||
| return result; | ||
| } | ||
| if (value instanceof Double) { | ||
| double d = (double) value; | ||
| if (d == 0.0) d = 0.0; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This warrants a code comment. |
||
| long bits = Double.doubleToLongBits(d); | ||
| return (int) (bits ^ (bits >>> 32)); | ||
| } | ||
| if (value instanceof Float) { | ||
| float f = (float) value; | ||
| if (f == 0.0f) f = 0.0f; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same. |
||
| return Float.floatToIntBits(f); | ||
| } | ||
| return value.hashCode(); | ||
| } | ||
|
|
||
| /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ | ||
| public static class FlutterError extends RuntimeException { | ||
|
|
@@ -142,15 +274,16 @@ public boolean equals(Object o) { | |
| return false; | ||
| } | ||
| MessageData that = (MessageData) o; | ||
| return Objects.equals(name, that.name) | ||
| && Objects.equals(description, that.description) | ||
| && code.equals(that.code) | ||
| && data.equals(that.data); | ||
| return pigeonDeepEquals(name, that.name) | ||
| && pigeonDeepEquals(description, that.description) | ||
| && pigeonDeepEquals(code, that.code) | ||
| && pigeonDeepEquals(data, that.data); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Capturing for posterity, this has the same inefficiency as the previous iteration, where we are feeding known-type values through a big type-inspection chain instead of dispatching to specific-type equality checkers. Not something we need to address now since we still mostly expect these to be used in tests where efficiency doesn't matter, but something to keep in mind later if it performance ever comes up. |
||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(name, description, code, data); | ||
| Object[] fields = new Object[] {getClass(), name, description, code, data}; | ||
| return pigeonDeepHashCode(fields); | ||
| } | ||
|
|
||
| public static final class Builder { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,9 @@ import java.nio.ByteBuffer | |
|
|
||
| private object EventChannelMessagesPigeonUtils { | ||
| fun deepEquals(a: Any?, b: Any?): Boolean { | ||
| if (a === b) { | ||
| return true | ||
| } | ||
| if (a is ByteArray && b is ByteArray) { | ||
| return a.contentEquals(b) | ||
| } | ||
|
|
@@ -24,20 +27,101 @@ private object EventChannelMessagesPigeonUtils { | |
| return a.contentEquals(b) | ||
| } | ||
| if (a is DoubleArray && b is DoubleArray) { | ||
| return a.contentEquals(b) | ||
| if (a.size != b.size) return false | ||
| for (i in a.indices) { | ||
| if (!deepEquals(a[i], b[i])) return false | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as Java, let's break out a helper to dispatch directly to here. |
||
| } | ||
| return true | ||
| } | ||
| if (a is FloatArray && b is FloatArray) { | ||
| if (a.size != b.size) return false | ||
| for (i in a.indices) { | ||
| if (!deepEquals(a[i], b[i])) return false | ||
| } | ||
| return true | ||
| } | ||
| if (a is Array<*> && b is Array<*>) { | ||
| return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } | ||
| return a.contentDeepEquals(b) | ||
| } | ||
| if (a is List<*> && b is List<*>) { | ||
| return a.size == b.size && a.indices.all { deepEquals(a[it], b[it]) } | ||
| } | ||
| if (a is Map<*, *> && b is Map<*, *>) { | ||
| return a.size == b.size && | ||
| a.all { (b as Map<Any?, Any?>).contains(it.key) && deepEquals(it.value, b[it.key]) } | ||
| if (a.size != b.size) return false | ||
| for (entry in a) { | ||
| val key = entry.key | ||
| var found = false | ||
| for (bEntry in b) { | ||
| if (deepEquals(key, bEntry.key)) { | ||
| if (deepEquals(entry.value, bEntry.value)) { | ||
| found = true | ||
| break | ||
| } else { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| if (!found) return false | ||
| } | ||
| return true | ||
| } | ||
| if (a is Double && b is Double) { | ||
| return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) | ||
| } | ||
| if (a is Float && b is Float) { | ||
| return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || | ||
| (a.isNaN() && b.isNaN()) | ||
| } | ||
| return a == b | ||
| } | ||
|
|
||
| fun deepHash(value: Any?): Int { | ||
| return when (value) { | ||
| null -> 0 | ||
| is ByteArray -> value.contentHashCode() | ||
| is IntArray -> value.contentHashCode() | ||
| is LongArray -> value.contentHashCode() | ||
| is DoubleArray -> { | ||
| var result = 1 | ||
| for (item in value) { | ||
| result = 31 * result + deepHash(item) | ||
| } | ||
| result | ||
| } | ||
| is FloatArray -> { | ||
| var result = 1 | ||
| for (item in value) { | ||
| result = 31 * result + deepHash(item) | ||
| } | ||
| result | ||
| } | ||
| is Array<*> -> value.contentDeepHashCode() | ||
| is List<*> -> { | ||
| var result = 1 | ||
| for (item in value) { | ||
| result = 31 * result + deepHash(item) | ||
| } | ||
| result | ||
| } | ||
| is Map<*, *> -> { | ||
| var result = 0 | ||
| for (entry in value) { | ||
| result += (deepHash(entry.key) xor deepHash(entry.value)) | ||
| } | ||
| result | ||
| } | ||
| is Double -> { | ||
| val d = if (value == 0.0) 0.0 else value | ||
| val bits = java.lang.Double.doubleToLongBits(d) | ||
| (bits xor (bits ushr 32)).toInt() | ||
| } | ||
| is Float -> { | ||
| val f = if (value == 0.0f) 0.0f else value | ||
| java.lang.Float.floatToIntBits(f) | ||
| } | ||
| else -> value.hashCode() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -61,16 +145,21 @@ data class IntEvent(val data: Long) : PlatformEvent() { | |
| } | ||
|
|
||
| override fun equals(other: Any?): Boolean { | ||
| if (other !is IntEvent) { | ||
| if (other == null || other.javaClass != javaClass) { | ||
| return false | ||
| } | ||
| if (this === other) { | ||
| return true | ||
| } | ||
| return EventChannelMessagesPigeonUtils.deepEquals(toList(), other.toList()) | ||
| val other = other as IntEvent | ||
| return EventChannelMessagesPigeonUtils.deepEquals(this.data, other.data) | ||
| } | ||
|
|
||
| override fun hashCode(): Int = toList().hashCode() | ||
| override fun hashCode(): Int { | ||
| var result = javaClass.hashCode() | ||
| result = 31 * result + EventChannelMessagesPigeonUtils.deepHash(this.data) | ||
| return result | ||
| } | ||
| } | ||
|
|
||
| /** Generated class from Pigeon that represents data sent in messages. */ | ||
|
|
@@ -89,16 +178,21 @@ data class StringEvent(val data: String) : PlatformEvent() { | |
| } | ||
|
|
||
| override fun equals(other: Any?): Boolean { | ||
| if (other !is StringEvent) { | ||
| if (other == null || other.javaClass != javaClass) { | ||
| return false | ||
| } | ||
| if (this === other) { | ||
| return true | ||
| } | ||
| return EventChannelMessagesPigeonUtils.deepEquals(toList(), other.toList()) | ||
| val other = other as StringEvent | ||
| return EventChannelMessagesPigeonUtils.deepEquals(this.data, other.data) | ||
| } | ||
|
|
||
| override fun hashCode(): Int = toList().hashCode() | ||
| override fun hashCode(): Int { | ||
| var result = javaClass.hashCode() | ||
| result = 31 * result + EventChannelMessagesPigeonUtils.deepHash(this.data) | ||
| return result | ||
| } | ||
| } | ||
|
|
||
| private open class EventChannelMessagesPigeonCodec : StandardMessageCodec() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this is called in a loop, I think it's worth breaking out a helper with the
doublelogic so that you can call that directly here.