Zastąp metodę pobierającą dla klasy danych Kotlin

99

Biorąc pod uwagę następującą klasę Kotlin:

data class Test(val value: Int)

Jak mogę zastąpić Intmetodę pobierającą, aby zwracała 0, jeśli wartość jest ujemna?

Jeśli nie jest to możliwe, jakie są techniki, aby osiągnąć odpowiedni wynik?

spierce7
źródło
14
Rozważ zmianę struktury kodu, tak aby wartości ujemne były konwertowane na 0, gdy tworzona jest instancja klasy, a nie w funkcji pobierającej. Jeśli zastąpisz metodę pobierającą w sposób opisany w odpowiedzi poniżej, wszystkie inne wygenerowane metody, takie jak equals (), toString () i dostęp do komponentów, nadal będą używać pierwotnej wartości ujemnej, co prawdopodobnie doprowadzi do zaskakującego zachowania.
yole,

Odpowiedzi:

148

Po spędzeniu prawie całego roku na codziennym pisaniu Kotlina stwierdziłem, że próba zastąpienia takich klas danych jest złą praktyką. Istnieją 3 ważne podejścia do tego zagadnienia, a po ich przedstawieniu wyjaśnię, dlaczego podejście sugerowane przez inne odpowiedzi jest złe.

  1. Miej logikę biznesową, która tworzy data classzmienną wartość na 0 lub większą przed wywołaniem konstruktora ze złą wartością. Jest to prawdopodobnie najlepsze podejście w większości przypadków.

  2. Nie używaj data class. Użyj zwykłego classi niech IDE wygeneruje dla Ciebie metody equalsi hashCode(lub nie, jeśli ich nie potrzebujesz). Tak, będziesz musiał ponownie wygenerować go, jeśli któraś z właściwości obiektu zostanie zmieniona, ale pozostaniesz z pełną kontrolą nad obiektem.

    class Test(value: Int) {
      val value: Int = value
        get() = if (field < 0) 0 else field
    
      override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Test) return false
        return true
      }
    
      override fun hashCode(): Int {
        return javaClass.hashCode()
      }
    }
    
  3. Utwórz dodatkową bezpieczną właściwość na obiekcie, który robi to, co chcesz, zamiast mieć wartość prywatną, która jest skutecznie zastępowana.

    data class Test(val value: Int) {
      val safeValue: Int
        get() = if (value < 0) 0 else value
    }
    

Złe podejście, które sugerują inne odpowiedzi:

data class Test(private val _value: Int) {
  val value: Int
    get() = if (_value < 0) 0 else _value
}

Problem z tym podejściem polega na tym, że klasy danych nie są tak naprawdę przeznaczone do zmiany danych w ten sposób. Tak naprawdę służą tylko do przechowywania danych. Zastępowanie metody pobierającej dla takiej klasy danych oznaczałoby to, Test(0)a Test(-1)nie equalnawzajem i miałyby różne hashCodes, ale po wywołaniu .valuemiałyby ten sam wynik. Jest to niespójne i chociaż może zadziałać dla Ciebie, inne osoby w Twoim zespole, które widzą, że jest to klasa danych, mogą przypadkowo użyć jej niewłaściwie, nie zdając sobie sprawy, jak ją zmieniłeś / sprawiłeś, że nie działa zgodnie z oczekiwaniami (tj. Takie podejście nie t działa poprawnie w a Maplub a Set).

spierce7
źródło
co z klasami danych używanymi do serializacji / deserializacji, spłaszczania zagnieżdżonej struktury? Np. Właśnie napisałem data class class(@JsonProperty("iss_position") private val position: Map<String, Double>) { val latitude = position["latitude"]; val longitude = position["longitude"] }i uważam to za całkiem dobre w moim przypadku, tbh. Co o tym myślisz? (było wiele innych pól i dlatego uważam, że nie ma sensu odtwarzać tej zagnieżdżonej struktury json w moim kodzie)
Antek
@Antek Biorąc pod uwagę, że nie zmieniasz danych, nie widzę nic złego w tym podejściu. Wspomnę również, że powodem, dla którego to robisz, jest to, że model po stronie serwera, który jesteś wysyłany, nie jest wygodny w użyciu na kliencie. Aby przeciwdziałać takim sytuacjom, mój zespół tworzy model po stronie klienta, na który tłumaczymy model po stronie serwera po deserializacji. Wszystko to zamykamy w interfejsie API po stronie klienta. Gdy zaczniesz otrzymywać przykłady, które są bardziej skomplikowane niż te, które pokazałeś, to podejście jest bardzo pomocne, ponieważ chroni klienta przed błędnymi decyzjami modelu serwera / interfejsami API.
spierce7
Nie zgadzam się z tym, co uważasz za „najlepsze podejście”. Problem, który widzę, polega na tym, że bardzo często chce się ustawić wartość w klasie danych i nigdy jej nie zmieniać. Na przykład parsowanie ciągu znaków w int. Niestandardowe metody pobierające / ustawiające w klasie danych są nie tylko przydatne, ale także niezbędne; w przeciwnym razie pozostajesz z POJO bean Java, które nic nie robią, a ich zachowanie + walidacja jest zawarte w innej klasie.
Abhijit Sarkar
Powiedziałem: „To prawdopodobnie najlepsze podejście w większości przypadków”. W większości przypadków, o ile nie zaistnieją pewne okoliczności, programiści powinni mieć wyraźny rozdział między swoim modelem a algorytmem / logiką biznesową, gdzie model wynikowy z ich algorytmu jasno przedstawia różne stany możliwych wyników. Kotlin jest do tego fantastyczny, z klasami zapieczętowanymi i klasami danych. Na przykład parsing a string into an intjasno zezwalasz na logikę biznesową analizowania i obsługi
nieliczbowych ciągów znaków
... Praktyka zmywania granicy między logiką modelu a logiką biznesową zawsze prowadzi do kodu trudniejszego w utrzymaniu i uważam, że jest anty-wzorcem. Prawdopodobnie 99% klas danych, które tworzę, jest niezmiennych / brakuje im seterów. Myślę, że naprawdę chciałbyś poświęcić trochę czasu na przeczytanie o korzyściach płynących z utrzymania niezmiennych modeli przez Twój zespół. Dzięki niezmiennym modelom mogę zagwarantować, że moje modele nie zostaną przypadkowo zmodyfikowane w jakimś innym losowym miejscu w kodzie, co zmniejsza skutki uboczne i ponownie prowadzi do kodu możliwego do utrzymania. czyli Kotlin się nie rozdzielił Listi MutableListbez powodu.
spierce7
31

Możesz spróbować czegoś takiego:

data class Test(private val _value: Int) {
  val value = _value
    get(): Int {
      return if (field < 0) 0 else field
    }
}

assert(1 == Test(1).value)
assert(0 == Test(0).value)
assert(0 == Test(-1).value)

assert(1 == Test(1)._value) // Fail because _value is private
assert(0 == Test(0)._value) // Fail because _value is private
assert(0 == Test(-1)._value) // Fail because _value is private
  • W klasie danych należy oznaczyć parametry konstruktora głównego za pomocą vallub var.

  • Przypisuję wartość _value do value, aby użyć żądanej nazwy właściwości.

  • Zdefiniowałem niestandardowy akcesor dla właściwości z logiką, którą opisałeś.

EPadronU
źródło
2
Wystąpił błąd w IDE, mówiący „Inicjator nie jest tutaj dozwolony, ponieważ ta właściwość nie ma pola zapasowego”
Cheng
6

Odpowiedź zależy od tego, z jakich funkcji faktycznie korzystasz data. @EPadron wspomniał o sprytnej sztuczce (ulepszonej wersji):

data class Test(private val _value: Int) {
    val value: Int
        get() = if (_value < 0) 0 else _value
}

Że będzie działa zgodnie z oczekiwaniami, ei ma jedno pole, jeden getter, prawda equals, hashcodei component1. Haczyk jest taki toStringi copysą dziwne:

println(Test(1))          // prints: Test(_value=1)
Test(1).copy(_value = 5)  // <- weird naming

Aby rozwiązać problem toString, możesz przedefiniować go ręcznie. Nie znam sposobu, aby naprawić nazewnictwo parametrów, ale w ogóle go nie używać data.

voddan
źródło
2

Wiem, że to stare pytanie, ale wydaje się, że nikt nie wspomniał o możliwości uczynienia wartości prywatną i pisania niestandardowego gettera w ten sposób:

data class Test(private val value: Int) {
    fun getValue(): Int = if (value < 0) 0 else value
}

Powinno to być całkowicie poprawne, ponieważ Kotlin nie wygeneruje domyślnego gettera dla pola prywatnego.

Ale poza tym zdecydowanie zgadzam się ze spierce7, że klasy danych służą do przechowywania danych i należy unikać zapisywania tam na sztywno logiki „biznesowej”.

bio007
źródło
Zgadzam się z twoim rozwiązaniem ale niż w kodzie musiałbyś to tak nazwać val value = test.getValue() a nie jak inne gettery val value = test.value
gori
Tak. To jest poprawne. Jest trochę inaczej, jeśli wywołujesz to z Javy, tak jak zawsze.getValue()
bio007
1

Widziałem twoją odpowiedź, zgadzam się, że klasy danych są przeznaczone tylko do przechowywania danych, ale czasami musimy coś z nich zrobić.

Oto, co robię z moją klasą danych. Zmieniłem niektóre właściwości z val na var i nadpisałem je w konstruktorze.

tak:

data class Recording(
    val id: Int = 0,
    val createdAt: Date = Date(),
    val path: String,
    val deleted: Boolean = false,
    var fileName: String = "",
    val duration: Int = 0,
    var format: String = " "
) {
    init {
        if (fileName.isEmpty())
            fileName = path.substring(path.lastIndexOf('\\'))

        if (format.isEmpty())
            format = path.substring(path.lastIndexOf('.'))

    }


    fun asEntity(): rc {
        return rc(id, createdAt, path, deleted, fileName, duration, format)
    }
}
Simou
źródło
Zmodyfikowanie pól tylko po to, aby można było je modyfikować podczas inicjalizacji, jest złą praktyką. Lepiej byłoby ustawić konstruktor jako prywatny, a następnie utworzyć funkcję, która będzie działać jako konstruktor (tj fun Recording(...): Recording { ... }.). Być może klasa danych nie jest tym, czego potrzebujesz, ponieważ w przypadku klas niebędących danymi możesz oddzielić swoje właściwości od parametrów konstruktora. Lepiej jest jasno określić swoje zamiary dotyczące zmienności w definicji klasy. Jeśli i tak zdarza się, że te pola są zmienne, klasa danych jest w porządku, ale prawie wszystkie moje klasy danych są niezmienne.
spierce7
@ spierce7 czy to naprawdę takie złe zasługiwać na negatywny głos? W każdym razie to rozwiązanie bardzo mi odpowiada, nie wymaga tyle kodowania i zachowuje hash i równość w stanie nienaruszonym.
Simou
0

To wydaje się być jedną (między innymi) irytującą wadą Kotlina.

Wydaje się, że jedynym rozsądnym rozwiązaniem, które całkowicie zachowuje wsteczną kompatybilność klasy, jest przekonwertowanie jej na klasę zwykłą (nie klasę "data") i ręczne (za pomocą IDE) implementowanie metod: hashCode ( ), equals (), toString (), copy () i componentN ()

class Data3(i: Int)
{
    var i: Int = i

    override fun equals(other: Any?): Boolean
    {
        if (this === other) return true
        if (other?.javaClass != javaClass) return false

        other as Data3

        if (i != other.i) return false

        return true
    }

    override fun hashCode(): Int
    {
        return i
    }

    override fun toString(): String
    {
        return "Data3(i=$i)"
    }

    fun component1():Int = i

    fun copy(i: Int = this.i): Data3
    {
        return Data3(i)
    }

}
Asher Stern
źródło
1
Nie jestem pewien, czy nazwałbym to wadą. Jest to jedynie ograniczenie funkcji klasy danych, która nie jest funkcją oferowaną przez Javę.
spierce7
0

Znalazłem następujące czynności, aby być najlepszym podejściem do osiągnięcia tego, co trzeba bez zerwania equalsi hashCode:

data class TestData(private var _value: Int) {
    init {
        _value = if (_value < 0) 0 else _value
    }

    val value: Int
        get() = _value
}

// Test value
assert(1 == TestData(1).value)
assert(0 == TestData(-1).value)
assert(0 == TestData(0).value)

// Test copy()
assert(0 == TestData(-1).copy().value)
assert(0 == TestData(1).copy(-1).value)
assert(1 == TestData(-1).copy(1).value)

// Test toString()
assert("TestData(_value=1)" == TestData(1).toString())
assert("TestData(_value=0)" == TestData(-1).toString())
assert("TestData(_value=0)" == TestData(0).toString())
assert(TestData(0).toString() == TestData(-1).toString())

// Test equals
assert(TestData(0) == TestData(-1))
assert(TestData(0) == TestData(-1).copy())
assert(TestData(0) == TestData(1).copy(-1))
assert(TestData(1) == TestData(-1).copy(1))

// Test hashCode()
assert(TestData(0).hashCode() == TestData(-1).hashCode())
assert(TestData(1).hashCode() != TestData(-1).hashCode())

Jednak,

Po pierwsze należy pamiętać, że _valueto varnieval , ale z drugiej strony, ponieważ jest to klasa prywatna i klasy danych nie mogą być dziedziczone z, dość łatwo jest upewnić się, że nie jest modyfikowana w klasie.

Po drugie, toString()daje nieco inny wynik niż w przypadku _valuenazwy value, ale jest spójny i TestData(0).toString() == TestData(-1).toString().

schatten
źródło
@ spierce7 Nie, tak nie jest. _valuejest modyfikowany w bloku init equalsi hashCode nie jest uszkodzony.
schatten
czy możesz mi pomóc z tym stackoverflow.com/questions/64015108/ ...
WISHY