Разве (на самом деле) плохо использовать case-классы для изменяемого состояния?

Рассмотрим следующий код:

case class Vector3(var x: Float, var y: Float, var z: Float)
{
  def add(v: Vector3): Unit =
  {
    this.x += v.x
    this.y += v.y
    this.z += v.z
  }
}

Как видите, case class имеет изменяемое состояние. Крайне не рекомендуется делать это, и обычно я согласен и абсолютно придерживаюсь этого «правила», но вот и вся история.

Я использую Scala, чтобы написать небольшой 3D-игровой движок с нуля. Итак, сначала я подумал об использовании (гораздо более) функционального стиля, но потом слишком часто срабатывал сборщик мусора.

Подумайте об этом на мгновение: у меня есть десятки и десятки сущностей в тестовой игре. Все они имеют положение (Вектор3), ориентацию (Вектор3), масштаб (Вектор3) и множество Матриц. Если бы я собирался сделать эти классы (Vector3 и Matrix4) функциональными и сделать их неизменяемыми, я бы возвращал сотни новых объектов в каждом кадре, что приводило бы к огромной потере кадров в секунду, потому что, скажем прямо, у GC есть свое применение, но в игровой движок, а с OpenGL... не очень.

Раньше Vector3 был классом, но теперь это case-класс, потому что где-то в коде мне нужно для него сопоставление с образцом.

Итак, действительно ли это плохо использовать case-класс, который содержит изменяемое состояние?

Пожалуйста, не превращайте это в дискуссию на тему "Почему вы вообще используете Scala для такого проекта?" Я знаю, что могут быть лучшие альтернативы, но я не заинтересован в написании (еще одного) движка на C++, и я не слишком горю желанием погрузиться в Rust (пока).


person Community    schedule 24.09.2015    source источник
comment
Я не знаю, есть ли хороший ответ на этот вопрос. Как вы сказали, крайне не рекомендуется реализовывать классы case с изменяемым состоянием, но, на мой взгляд, это соглашение, о котором вам следует помнить при реализации библиотеки. В таком случае может быть довольно неприятно, если вы можете изменить состояние класса case там, где этого никто не ожидает. Но если вы хорошо это задокументируете, и это достаточно важно для необходимой производительности, вы можете использовать доступные функции.   -  person M. Reif    schedule 24.09.2015
comment
Просто стороннее мнение: когда дело доходит до низкоуровневого программирования, в дикой природе существует бесчисленное множество примеров, которые отходят от соглашений и устоявшихся способов ведения дел. Хотите верьте, хотите нет, но в C++ тоже есть соглашения и хорошие практики, но эти хорошие практики и удобочитаемость часто приносятся в жертву оптимизации производительности.   -  person Ruslan    schedule 24.09.2015
comment
взгляните на этот вопрос: stackoverflow.com/questions/4653424/   -  person anquegi    schedule 24.09.2015


Ответы (1)


Я бы сказал, что плохо использовать классы case с изменяемым состоянием, но только потому, что они переопределяют ваши методы equals и hashCode. Где-то в вашем коде вы можете проверить, являются ли a == b и узнать, что они равны. Позже они могут быть другими, потому что они изменчивы. По крайней мере, их опасно использовать в сочетании с коллекциями на основе хэшей.

Однако, похоже, вам не нужны все функции, предоставляемые классом case. Что вам действительно нужно, так это экстрактор для сопоставления с образцом, так почему бы не определить его? Кроме того, статическая фабрика apply и удобочитаемое toString-представление могут быть удобными, поэтому вы можете их реализовать.

Как насчет:

class Vector (var x: Float, var y: Float, var z: Float) {
  override def toString = s"Vector($x, $y, $z)"
}

object Vector {
  def apply(x: Float, y: Float, z: Float) = new Vector(x, y, z)

  def unapply(v: Vector): Option[(Float, Float, Float)] = Some((v.x, v.y, v.z))
}
person Kulu Limpa    schedule 24.09.2015
comment
Было бы лучше просто использовать готовые к использованию коллекции линейной алгебры ( " title=" обеспечивает ли скалала простой способ вставки вектора в матрицу"> stackoverflow.com/questions/12449427/ ) вместо определения собственного вектора - person ayvango; 25.09.2015
comment
Спасибо, у меня уже есть такой удобный toString-метод. Применить - это все, что мне нужно для сопоставления с образцом? Думаю, мне тоже понадобятся equals и hashCode? - person ; 25.09.2015
comment
@ayvango: я хочу узнать что-то новое. Чтобы узнать что-то новое, я должен экспериментировать. В таком случае я хочу улучшить свой Scala и выучить еще компьютерную графику за тот же прогон. Я уже писал движок на C++, но там использовал GLM. Я также использовал libgdx и тому подобное в прошлом. Теперь я хочу погрузиться глубже, и я уже реализовал большую часть математики и проверил, что это атомарно правильно. - person ; 25.09.2015
comment
@Teolha, это просто вопрос распределения времени. Вы можете сосредоточиться на повторной реализации линейной алгебры или потратить время на некоторые продвинутые функции игрового движка. Например, есть много разных способов наложения теней. И вы можете попробовать несколько подходов, если у вас будет свободное время. - person ayvango; 25.09.2015
comment
@ayvango Спасибо, я принял решение еще до того, как начал программировать. Нет смысла это обсуждать :) - person ; 25.09.2015
comment
@Teolha Вам не нужны методы equals и hashCode для изменяемых типов. При необходимости вы можете реализовать вспомогательный метод, чтобы проверить, имеют ли два экземпляра в настоящее время одно и то же значение. Для сопоставления с образцом вам нужно только реализовать unapply. - person Kulu Limpa; 26.09.2015