操作数据

数据的访问和修改

一个数据操纵器代表一系列内聚的数据。其存储了这些数据的表现形式,并可以通过匹配的数据访问器创建或设置回数据访问器。我们再次使用一个示例,同时再次使用“治疗”作为示例。

代码示例:通过数据操纵器的方式进行“治疗”

import org.spongepowered.api.data.DataHolder;
import org.spongepowered.api.data.DataTransactionResult;
import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;
import org.spongepowered.api.data.value.mutable.MutableBoundedValue;

import java.util.Optional;

public static void heal(DataHolder target) {
    Optional<HealthData> healthOptional = target.get(HealthData.class);
    if (healthOptional.isPresent()) {
        HealthData healthData = healthOptional.get();

        double maxHealth = healthData.maxHealth().get();
        MutableBoundedValue<Double> currentHealth = healthData.health();
        currentHealth.set(maxHealth);
        healthData.set(currentHealth);

        target.offer(healthData);
    }
}

首先,我们需要检查我们的目标是否具有生命值的数据,我们首先通过把 HealthData.class 作为 get() 方法的参数的方式传入,以获取一个 Optional 用于检查。如果我们的数据访问器并不支持 HealthData ,那么它会不包含任何数据。

如果我们的数据访问器支持,那么它就包含了一个可修改的来源于当前数据访问器的数据副本。然后我们完成我们的改动,并设置回当前数据访问器(又一次地,我们使用了 offer() 方法,这一方法同样会返回一个 DataTransactionResult ,这一返回值我们暂且不去管它,稍后我们会在 这里 看到这一返回值的具体含义)。

正如你所看到的那样,我们从 DataHolder 获取的 health()maxHealth() 又一次地以被包装起来的数据值形式展现在我们面前。因为我们从 health() 获取到的 MutableBoundedValue 仍然只是一份数据的副本,所以我们首先需要先把修改过的数据设置回我们的 DataManipulator ,然后再设置回当前数据访问器。

小技巧

数据 API 第一约定:你获取到的一切都是一份数据副本。所以每当你做出什么改变,你都需要把你的改变后的数据设置回它原先被获取到的地方。

另一个可能的修改是直接移除掉 DataManipulator ,通过向 remove() 方法传入对应的 Class 我们就可以很容易地做到这一点。如果对不能被移除的 DataManipulator 执行移除操作,就会导致返回一个表示操作失败的 DataTransactionResult 。下面的代码尝试从给定的 DataHolder 删除掉一个自定义的名称,然后我们再一次忽略返回值。

代码示例:移除自定义显示名称

import org.spongepowered.api.data.manipulator.mutable.DisplayNameData;

public void removeName(DataHolder target) {
    target.remove(DisplayNameData.class);
}

数据操纵器(DataManipulator)与数据键(Key)的比较

如果你比较一下我们提供的这两个“治疗”的例子,你会纠结“使用数据键是如此简单,那么数据操纵器又有什么用呢”。这句话大多数情况下都是对的——在你设置单个数据的时候。不过数据操纵器的真正优点是在其包含了同一个类型的 所有 数据。下面是另一个示例:

代码示例:交换两个数据访问器的生命值

public void swapHealth(DataHolder targetA, DataHolder targetB) {
    if (targetA.supports(HealthData.class) && targetB.supports(HealthData.class)) {
        HealthData healthA = targetA.getOrCreate(HealthData.class).get();
        HealthData healthB = targetB.getOrCreate(HealthData.class).get();
        targetA.offer(healthB);
        targetB.offer(healthA);
    }
}

如果两个数据访问器都支持 HealthData ,那么我们就把所有和生命值相关的数据存进了一个变量。这次我们根本不需要担心 Optional ,因为我们首先判断了两个数据访问器是否支持 HealthData ,其次我们使用了 getOrCreate() 方法,以保证如果 HealthData 不存在,就自动创建一个默认值。

然后我们只是把彼此的 HealthData 设置到了对方的数据访问器上,从而交换了它们俩的生命值。

如果直接使用 Keys 解决问题,代码会变得冗长和复杂,因为我们需要关心每一个数据键的相关问题。如果我们想要交换的不是生命值而是别的数据(诸如 InvisibilityData ,它包含了一个列表的数据),那么我们有可能就会不得不做很多工作。不过由于数据访问器只关心存储的数据这一整体而不在意其内容,我们甚至可以稍稍修改一下上面的方法使得两个数据访问器之间可以交换任意的数据。

代码示例:交换两个数据访问器的任意数据

import org.spongepowered.api.data.manipulator.DataManipulator;

public  <T extends DataManipulator<?,?>> void swapData(DataHolder targetA, DataHolder targetB, Class<T> dataClass) {
   if (targetA.supports(dataClass) && targetB.supports(dataClass)) {
       T dataA = targetA.getOrCreate(dataClass).get();
       T dataB = targetB.getOrCreate(dataClass).get();
       targetA.offer(dataB);
       targetB.offer(dataA);
   }
}

我们得以写一个方法,使得其可以交换两个数据访问器的任意数据,这恰恰体现了数据 API 的设计理念:最大程度上的兼容性。

可变(Mutable)和不可变(Immutable)数据比较

对于任何一个数据操纵器,我们都可以获取对应的 ImmutableDataManipulator 。比如 HealthDataImmutableHealthData 保存着完全相同的数据类型,区别仅仅是后者在修改数据时返回一个新的实例。

使用 asImmutable()asMutable() 两个方法就可以很方便地在可变数据和不可变数据之间进行转换。每个方法返回的都是数据的副本。唯一的一种获取不可变数据的方式是首先通过数据访问器获取可变数据,然后使用 asImmutable() 进行转换。

不可变数据的一种可能的用途是一些诸如玩家被治疗时触发的事件。这一事件需要提供生命值数据的副本,然而不应该有办法修改它。这样我们就可以在事件中提供 ImmutableHealthData 以解决问题。即便我们不知道第三方代码会对 ImmutableHealthData 做什么,我们仍然可以保证拿到的数据是不会发生变化的。

不存在的数据

如上文所述, get() 方法可能会返回不包括任何数据的 Optional ,这是因为:

  • 当前 DataHolder 不支持给定的 DataManipulator
  • 当前 DataHolder 虽然支持给定的 DataManipulator ,但其并没有存有任何的数据

不存在的数据和含有默认值的数据,在语义上有一个很大的差别。虽然我们始终可以指定一个默认值,但仍然有可能有 DataHolder 支持一种类型的数据却并不存有任何数据的情况,下面是一些例子:

  • HealthData 总是存在的,只要(原版的) DataHolder 支持它
  • DisplayNameDataPlayer 身上总是存在的,不过其他实体就不见得了。