如何区分 NULL 值的 "unset" 和 "no-assignment"?

How to distinguish between "unset" and "no-assignment" for NULL values?

在这个例子中的php中——但实际上在一般编程中,有没有办法区分"no-assignment""unset-value" 合并 2 个相同类型的不可变数据对象时 null 值的命令?

考虑这个 php class 这是一个不可变的数据对象。它在其构造函数中接受一个字符串和一个整数,并且只为值提供访问器:

class Data
{
    protected $someNumber;
    protected $someString;

    public function __construct(?int $someNumber, ?string $someString)
    {
        $this->someNumber = $someNumber;
        $this->someString = $someString;
    }

    public function getSomeNumber(): ?int
    {
        return $this->someNumber;
    }

    public function getSomeString(): ?string
    {
        return $this->someString;
    }
}

这些值始终可以是 null 或其各自的数据类型 stringinteger。此外,构造函数接受 null 值而不是 string 和/或 int:UNSET 操作。

现在我希望能够合并 Data 的 2 个实例,类似于接受 $first$second 的简化工厂方法,其中数据在 $second覆盖 $first 中的数据(如果存在)。

class DataFactory
{
    public function merge(Data $first, Data $second): Data
    {
        // Uses data from $first if corresponding data from
        // $second is (strictly) null
        return new Data(
            $second->getSomeNumber() ?? $first->getSomeNumber(),
            $second->getSomeString() ?? $first->getSomeString(),
    }
}

在上面的示例中,$second 的访问器返回的 null 值被解释为 NO UPDATE 操作:当遇到 null 时,对应的 $first 的值保持。问题是我希望能够区分 merge.

内的 NO UPDATE 操作或 UNSET 操作请求

Data class 中的严格类型不允许使用某种字符串常量,如 "DATA_UNSET_FIELD" 作为标记值,​​因此直接在数据本身上实现它似乎不可能的。甚至因为为任何值传递构造函数 null 肯定意味着 SET NULL。

我正在考虑某种更新镜头,它明确指定合并时应该取消设置的属性,这样 $second 中的 null 值就意味着没有更新(保持 $first).

什么是紧凑的面向对象模式来解决这个问题?我已经可以想象像随着数据增长爆炸普通数组模式或 class 策略 classes 爆炸这样的问题。此外,我有点担心 Data 对象的 "mobility",因为新对象必须在某些时候与它们相关联。

提前致谢!


编辑

我希望能够区分不覆盖当前值和取消设置一个值——即分配null – 合并 Data 的 2 个实例时,其中 $first 是基数,$second 覆盖 $first 的数据。作为一个细节,合并结果是第三个新对象,即合并结果。

查看 DataFactory 片段 null 当前 $second 中的值被解释为 "keep the corresponding value of $first"。但是我如何为每个字段携带另一个标志,指示在结果对象中应该将哪些字段设置为 null,以一种干净的方式,并且不会过多地混淆数据 class?

PHP 无法区分未赋值变量和空变量。 这使得跟踪哪些属性应该被覆盖变得不可避免。

我看到你有两个顾虑:

  • 保持Data不变
  • 保持 Data 的界面干净(例如强制执行严格类型)

能够跟踪 "defined" 和 "undefined" 属性的最简单的数据结构之一是 \stdClass 对象(但数组也非常好)。 通过将 merge() 方法移动到 Data class 中,您将能够隐藏任何实现细节 - 保持界面整洁。

实现可能如下所示:

final class Data {

    /** @var \stdClass */
    protected $props;

    // Avoid direct instantiation, use ::create() instead
    private function __construct()
    {
        $this->props = new \stdClass();
    }

    // Fluent interface
    public static function create(): Data
    {
        return new self();
    }

    // Enforce immutability
    public function __clone()
    {
        $this->props = clone $this->props;
    }

    public function withSomeNumber(?int $someNumber): Data
    {
        $d = clone $this;
        $d->props->someNumber = $someNumber;
        return $d;
    }

    public function withSomeString(?string $someString): Data
    {
        $d = clone $this;
        $d->props->someString = $someString;
        return $d;
    }

    public function getSomeNumber(): ?int
    {
        return $this->props->someNumber ?? null;
    }

    public function getSomeString(): ?string
    {
        return $this->props->someString ?? null;
    }

    public static function merge(...$dataObjects): Data
    {
        $final = new self();

        foreach ($dataObjects as $data) {
            $final->props = (object) array_merge((array) $final->props, (array) $data->props);
        }

        return $final;
    }
}


$first = Data::create()
    ->withSomeNumber(42)
    ->withSomeString('foo');

// Overwrite both someNumber and someString by assigning null
$second = Data::create()
    ->withSomeNumber(null)
    ->withSomeString(null);

// Overwrite "someString" only
$third = Data::create()
    ->withSomeString('bar');

$merged = Data::merge($first, $second, $third); // Only "someString" property is set to "bar"
var_dump($merged->getSomeString()); // "bar"