678 lines
22 KiB
Markdown
678 lines
22 KiB
Markdown
# Structive Property Core Guide
|
|
|
|
[中文](CORE_GUIDE.zh-CN.md)
|
|
|
|
## 1. Include and CMake target
|
|
|
|
```cpp
|
|
#include <structive/property/property.hpp>
|
|
```
|
|
|
|
```cmake
|
|
target_link_libraries(my_target PRIVATE structive::property_core)
|
|
```
|
|
|
|
Property Core is C++20.
|
|
|
|
## 2. Define a managed object
|
|
|
|
```cpp
|
|
using namespace structive;
|
|
struct Device : Property_Object<Device> {
|
|
double temperature{25.0};
|
|
double pressure{101.3};
|
|
int serial_number{1001};
|
|
};
|
|
```
|
|
|
|
`Property_Object<Device>` adds managed operations. The fields remain ordinary members.
|
|
|
|
## 3. Define the type descriptor
|
|
|
|
```cpp
|
|
template <>
|
|
struct structive::Type_Descriptor<Device> {
|
|
static auto get() {
|
|
return object<Device>(
|
|
synchronization(sync_all_independent),
|
|
field<&Device::temperature>(
|
|
key<"temperature">,
|
|
unit<"C">,
|
|
min_value<-50.0>,
|
|
max_value<200.0>
|
|
),
|
|
field<&Device::pressure>(
|
|
key<"pressure">,
|
|
unit<"kPa">
|
|
),
|
|
field<&Device::serial_number>(
|
|
key<"serial_number">,
|
|
read_only
|
|
)
|
|
);
|
|
}
|
|
};
|
|
```
|
|
|
|
The descriptor is the structural definition of `Device`.
|
|
|
|
### 3.1 Type-level metadata
|
|
|
|
Extensions can attach compile-time attributes to the described type itself, independently
|
|
from property metadata and inheritable property defaults:
|
|
|
|
```cpp
|
|
struct Action_Category {};
|
|
struct Action {
|
|
using attribute_category = Action_Category;
|
|
static constexpr bool single_valued = false;
|
|
static constexpr bool inheritable = false;
|
|
std::string_view name;
|
|
};
|
|
|
|
return object<Device>(
|
|
type_metadata(Action{"reset"}, Action{"calibrate"}),
|
|
field<&Device::temperature>(key<"temperature">)
|
|
);
|
|
```
|
|
|
|
Use `Schema::type_attribute_count<Category>`, `Schema::has_type_attribute<Category>`,
|
|
`schema.type_attribute<Category>()` for a single-valued category, and
|
|
`schema.for_each_type_attribute<Category>(callback)` for traversal. Core stores and
|
|
validates the generic Attribute protocol but does not interpret extension-owned categories.
|
|
Type metadata is not a property default and is never copied into individual properties.
|
|
|
|
## 4. Schema guarantees
|
|
|
|
A valid schema guarantees:
|
|
|
|
- every registered property has a non-empty key;
|
|
- keys are unique;
|
|
- member selectors belong to the described object type;
|
|
- single-valued Attribute categories are not duplicated on the same property;
|
|
- capability metadata cannot request operations unsupported by the underlying accessor;
|
|
- constraints are compatible with the property value type.
|
|
|
|
Use:
|
|
|
|
```cpp
|
|
static_assert(Property_Described_Object<Device>);
|
|
using Schema = type_descriptor_schema_t<Device>;
|
|
static_assert(Valid_Property_Schema<Schema>);
|
|
```
|
|
|
|
## 5. Access the schema
|
|
|
|
```cpp
|
|
const auto& schema = type_descriptor<Device>();
|
|
const auto& same_schema = device.schema();
|
|
```
|
|
|
|
Properties can be selected by index or member pointer:
|
|
|
|
```cpp
|
|
const auto& first = schema.property<0>();
|
|
const auto& temperature = schema.property<&Device::temperature>();
|
|
```
|
|
|
|
## 6. Property descriptor information
|
|
|
|
A property descriptor exposes compile-time structural facts:
|
|
|
|
```cpp
|
|
using Property = std::remove_cvref_t<decltype(schema.property<&Device::temperature>())>;
|
|
static_assert(Property::readable);
|
|
static_assert(Property::writable);
|
|
static_assert(Property::runtime_copy_writable);
|
|
using Value = Property::value_type;
|
|
using Accessor = Property::accessor_type;
|
|
using Dependencies = Property::dependency_spec;
|
|
```
|
|
|
|
Every custom `Property_Accessor` must declare `using dependency_spec = ...`. Use `No_Property_Dependencies` when it has no dependencies; there is no implicit fallback.
|
|
|
|
A `read_only` field reports:
|
|
|
|
```cpp
|
|
using Serial = std::remove_cvref_t<decltype(schema.property<&Device::serial_number>())>;
|
|
static_assert(Serial::readable);
|
|
static_assert(!Serial::writable);
|
|
```
|
|
|
|
The key is available at runtime without dynamic allocation:
|
|
|
|
```cpp
|
|
auto key_value = schema.property<&Device::temperature>().key();
|
|
```
|
|
|
|
## 7. Intrinsic capability
|
|
|
|
Core capability values are:
|
|
|
|
```cpp
|
|
Property_Capability::none
|
|
Property_Capability::read
|
|
Property_Capability::write
|
|
Property_Capability::read_write
|
|
```
|
|
|
|
Convenience Attributes are:
|
|
|
|
```cpp
|
|
read_only
|
|
write_only
|
|
read_write
|
|
inaccessible
|
|
```
|
|
|
|
The capability belongs to the property itself. It is not an authorization rule.
|
|
|
|
If capability metadata is absent, Structive derives capability from the accessor.
|
|
|
|
For a normal non-const member field, the default is read/write.
|
|
|
|
For a getter-only computed property, the default is read-only.
|
|
|
|
Capability metadata can narrow the accessor, but it cannot create an operation the accessor does not support.
|
|
|
|
## 8. No access-control API
|
|
|
|
Property Core intentionally does not provide access modes or domain views.
|
|
|
|
There is no built-in distinction between:
|
|
|
|
```text
|
|
internal
|
|
external
|
|
persistence
|
|
```
|
|
|
|
A consumer decides its own policy. For example, a GUI may choose to expose only selected properties even though all of them are structurally readable.
|
|
|
|
Core only exposes intrinsic `readable` and `writable` facts.
|
|
|
|
## 9. Core Attributes
|
|
|
|
### 9.1 Key
|
|
|
|
Every property requires one key:
|
|
|
|
```cpp
|
|
key<"temperature">
|
|
```
|
|
|
|
The key is the structural protocol identifier used by runtime lookup and adapters.
|
|
|
|
### 9.2 Capability
|
|
|
|
```cpp
|
|
read_only
|
|
write_only
|
|
read_write
|
|
inaccessible
|
|
```
|
|
|
|
Capability Attributes are single-valued and non-inheritable.
|
|
|
|
### 9.3 Unit
|
|
|
|
```cpp
|
|
unit<"C">
|
|
unit<"kPa">
|
|
```
|
|
|
|
Core stores unit metadata but does not perform conversion.
|
|
|
|
### 9.4 Sensitive
|
|
|
|
```cpp
|
|
sensitive<>
|
|
sensitive<false>
|
|
```
|
|
|
|
`sensitive` is inheritable metadata. It does not implement authorization. A consumer may use it as one input to its own policy.
|
|
|
|
## 10. Custom Attributes and defaults
|
|
|
|
Any type following the Attribute protocol can be attached to a property.
|
|
|
|
```cpp
|
|
struct Group_Category {};
|
|
template <Fixed_String Value>
|
|
struct Group_Attribute {
|
|
using attribute_category = Group_Category;
|
|
static constexpr bool single_valued = true;
|
|
static constexpr bool inheritable = true;
|
|
static constexpr auto value = Value;
|
|
};
|
|
```
|
|
|
|
Inheritable Attributes can be supplied by `defaults(...)`:
|
|
|
|
```cpp
|
|
object<Device>(
|
|
defaults(sensitive<>),
|
|
field<&Device::temperature>(key<"temperature">)
|
|
)
|
|
```
|
|
|
|
Capability is intentionally non-inheritable because each property owns its intrinsic operation set.
|
|
|
|
## 11. Constraints and validation
|
|
|
|
Built-in constraints include:
|
|
|
|
```cpp
|
|
min_value<0>
|
|
max_value<100>
|
|
finite
|
|
```
|
|
|
|
Custom constraints:
|
|
|
|
```cpp
|
|
constraint<"even">([](int value) {
|
|
return value % 2 == 0;
|
|
})
|
|
```
|
|
|
|
Validation is explicit:
|
|
|
|
```cpp
|
|
auto result = validate_property_value<&Device::temperature>(device.schema(), candidate);
|
|
if (result) {
|
|
auto key_value = result->property_key;
|
|
auto code = result->code;
|
|
}
|
|
```
|
|
|
|
`write()` does not automatically call validation.
|
|
|
|
## 12. Typed managed read/write
|
|
|
|
Member-pointer access is the preferred C++ API:
|
|
|
|
```cpp
|
|
auto temperature = device.read<&Device::temperature>();
|
|
device.write<&Device::temperature>(30.0);
|
|
```
|
|
|
|
Compile-time key access is also available:
|
|
|
|
```cpp
|
|
auto temperature = device.read<"temperature">();
|
|
device.write<"temperature">(30.0);
|
|
```
|
|
|
|
The typed interfaces are constrained by intrinsic capability. A write to `read_only` does not participate in overload resolution.
|
|
|
|
```cpp
|
|
template <class Object>
|
|
concept Can_Write_Serial = requires(Object& object) {
|
|
object.template write<&Device::serial_number>(1);
|
|
};
|
|
static_assert(!Can_Write_Serial<Device>);
|
|
```
|
|
|
|
## 13. Read-only fast path
|
|
|
|
Stored read-only properties are statically removed from managed locking.
|
|
|
|
Given:
|
|
|
|
```cpp
|
|
field<&Device::serial_number>(key<"serial_number">, read_only)
|
|
```
|
|
|
|
Structive resolves:
|
|
|
|
```text
|
|
serial_number -> unsynchronized_slot
|
|
```
|
|
|
|
and `read<&Device::serial_number>()` does not query a lock slot or construct a `shared_lock`.
|
|
|
|
This remains true even when the broad default is `sync_all_shared`.
|
|
|
|
If an object has only stored read-only properties, those properties contribute zero mutexes to `resolved_synchronization().lock_count`.
|
|
|
|
## 14. Synchronization plans
|
|
|
|
Synchronization is intentionally a topology layer rather than a property permission system. It answers only one question: when managed mutable state is accessed concurrently, which properties share a consistency domain?
|
|
|
|
Stored intrinsic read-only properties are removed before lock slots are materialized. A broad rule such as `sync_all_shared` therefore never creates a mutex merely for a stored `read_only` property.
|
|
|
|
### 14.1 Default topologies
|
|
|
|
| Default | Meaning for properties that require synchronization |
|
|
| --- | --- |
|
|
| `sync_all_independent` | each property receives its own lock domain |
|
|
| `sync_all_shared` | all properties share one lock domain |
|
|
| `sync_all_unsynchronized` | no real lock domain is created |
|
|
|
|
```cpp
|
|
synchronization(sync_all_independent)
|
|
```
|
|
|
|
`sync_all_unsynchronized` is an explicit opt-out. Structive still provides managed access, but the caller owns the thread-safety consequences of concurrent reads and writes.
|
|
|
|
### 14.2 Compile-time member rules
|
|
|
|
Use member pointers when the schema is known in C++ code:
|
|
|
|
```cpp
|
|
synchronization(
|
|
sync_all_shared,
|
|
sync_independent<&Device::temperature>(),
|
|
sync_unsynchronized<&Device::debug_counter>()
|
|
)
|
|
```
|
|
|
|
Member validity is checked while the synchronization specification is materialized for the schema.
|
|
|
|
### 14.3 Runtime-key rules
|
|
|
|
Adapters or configuration code may build a `Synchronization_Plan` from keys:
|
|
|
|
```cpp
|
|
Synchronization_Plan plan;
|
|
plan.set_default(Synchronization_Default::independent);
|
|
plan.unsynchronized("debug_counter");
|
|
```
|
|
|
|
Key validity cannot be known until the plan is resolved. Unknown properties, duplicate property configuration, empty groups and duplicate group names are rejected with `std::invalid_argument`.
|
|
|
|
### 14.4 Groups are consistency domains
|
|
|
|
```cpp
|
|
synchronization(
|
|
sync_all_independent,
|
|
sync_group<&Device::min_speed, &Device::max_speed>("speed_range")
|
|
)
|
|
```
|
|
|
|
Members in one group resolve to the same lock slot if they actually require synchronization. A read-only stored member may appear in a broad rule or group, but it still resolves to `unsynchronized_slot` because there is no managed writer to protect.
|
|
|
|
A group should express a real invariant or snapshot boundary. It should not be used merely to reduce the mutex count.
|
|
|
|
### 14.5 Why the synchronization API has several forms
|
|
|
|
The forms represent different information availability, not duplicate concepts:
|
|
|
|
- type-level defaults describe the normal topology once per object type;
|
|
- compile-time member rules give typed C++ code compile-time schema checking;
|
|
- runtime-key rules support adapters that discover property names dynamically;
|
|
- per-instance overrides support objects whose synchronization topology genuinely differs from the type default;
|
|
- guards express a temporary multi-property consistency operation.
|
|
|
|
The common semantic model is always the same resolved lock-slot topology.
|
|
|
|
## 15. Per-instance synchronization override
|
|
|
|
An object can explicitly override the type default:
|
|
|
|
```cpp
|
|
Device device{
|
|
property_synchronization(
|
|
synchronization(sync_all_shared)
|
|
)
|
|
};
|
|
```
|
|
|
|
For typed member rules:
|
|
|
|
```cpp
|
|
Device device{
|
|
property_synchronization<Device>(
|
|
synchronization(
|
|
sync_all_independent,
|
|
sync_group<&Device::temperature, &Device::pressure>("environment")
|
|
)
|
|
)
|
|
};
|
|
```
|
|
|
|
The default topology is resolved once and shared per type. Only an object with an explicit override stores a compact override layout. Copy and move construction preserve an object's override topology; assignment preserves the destination object's existing topology because assignment changes object state, not the synchronization policy chosen for that instance.
|
|
|
|
## 16. Inspect resolved synchronization
|
|
|
|
```cpp
|
|
auto view = device.resolved_synchronization();
|
|
auto count = view.lock_count;
|
|
```
|
|
|
|
Inspect a member slot:
|
|
|
|
```cpp
|
|
auto slot = device.lock_slot<&Device::temperature>();
|
|
```
|
|
|
|
No-lock properties use:
|
|
|
|
```cpp
|
|
Resolved_Synchronization_View::unsynchronized_slot
|
|
```
|
|
|
|
For a stored read-only property this is automatic. `resolved_synchronization()` and `lock_slot()` are primarily diagnostics and framework-level inspection APIs; normal business code should usually express its intent through `read`, `write`, `lock_shared` and `lock_unique` instead of reasoning about numeric slots.
|
|
|
|
## 17. Static multi-property guards
|
|
|
|
Read guard:
|
|
|
|
```cpp
|
|
auto guard = device.lock_shared<&Device::temperature, &Device::pressure>();
|
|
auto by_member = guard.get<&Device::temperature>();
|
|
auto by_key = guard.get<"pressure">();
|
|
```
|
|
|
|
Write guard:
|
|
|
|
```cpp
|
|
auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>();
|
|
guard.set<&Device::min_speed>(20);
|
|
guard.set<"maximum_speed">(120);
|
|
```
|
|
|
|
Member and compile-time-key access intentionally use the same `get`/`set` names. Capability constraints are part of the overload itself: a typed unique guard cannot be requested for a read-only property, and `set` does not exist for a non-writable property in a `requires` expression.
|
|
|
|
Static guards resolve member slots, deduplicate repeated domains and acquire locks in stable numeric slot order. Request order therefore does not become mutex acquisition order, avoiding lock-order inversion when two callers request the same domains in different member order.
|
|
|
|
A read-only stored property may participate in a shared guard's logical held set without introducing a mutex. This allows a guard API to read it consistently with its intrinsic contract while preserving the read-only zero-lock fast path.
|
|
|
|
## 18. Dynamic-key guards
|
|
|
|
```cpp
|
|
auto read_guard = device.lock_shared({"temperature", "pressure"});
|
|
auto write_guard = device.lock_unique({"temperature", "pressure"});
|
|
auto value = read_guard.get<"temperature">();
|
|
```
|
|
|
|
Dynamic guards exist for callers whose selected property set is known only at runtime. Because the keys are dynamic, errors that typed guards reject through constraints become runtime errors:
|
|
|
|
- an unknown key throws `std::invalid_argument`;
|
|
- `lock_shared` rejects a non-readable property;
|
|
- `lock_unique` rejects a non-writable property;
|
|
- a later `get`/`set` throws `std::logic_error` if the requested property is outside the guard's held set.
|
|
|
|
Dynamic and static guards use the same resolved lock topology and the same stable lock ordering.
|
|
|
|
The behavior above is covered directly by `core/tests/synchronization_test.cpp`, including independent/shared/unsynchronized defaults, groups, overrides, invalid plans, runtime-key guards, blocking behavior and reversed member-order acquisition.
|
|
|
|
## 19. Traversal
|
|
|
|
Schema-only traversal:
|
|
|
|
```cpp
|
|
schema.for_each_property([&](auto index, const auto& descriptor) {
|
|
});
|
|
```
|
|
|
|
Readable value traversal:
|
|
|
|
```cpp
|
|
device.for_each_readable([&](auto index, const auto& descriptor, const auto& value) {
|
|
});
|
|
```
|
|
|
|
Locked readable traversal:
|
|
|
|
```cpp
|
|
device.for_each_readable_locked([&](auto index, const auto& descriptor, const auto& value) {
|
|
});
|
|
```
|
|
|
|
The locked traversal acquires only actual synchronization slots. Read-only stored properties do not introduce locks.
|
|
|
|
Writable transaction-style guard acquisition:
|
|
|
|
```cpp
|
|
device.with_all_writable_locked([&](auto& guard) {
|
|
});
|
|
```
|
|
|
|
This acquires all writable managed lock domains; it does not perform validation or rollback automatically.
|
|
|
|
## 20. Computed properties
|
|
|
|
A synchronized computed property receives a read view:
|
|
|
|
```cpp
|
|
computed_property<Device, int>(depends_on<&Device::min_speed, &Device::max_speed>, [](const auto& view) {
|
|
return view.template get<&Device::max_speed>() - view.template get<&Device::min_speed>();
|
|
}, key<"speed_span">)
|
|
```
|
|
|
|
`depends_on<...>` makes dependencies explicit Schema facts. `depends_on_keys<"a", "b">` is available when key-based declaration is more appropriate. `depends_on<>` is a valid explicit zero-dependency declaration. A computed view can read only its declared direct dependencies.
|
|
|
|
Writable dependencies that must form one snapshot should share a synchronization domain with each other:
|
|
|
|
```cpp
|
|
synchronization(
|
|
sync_all_independent,
|
|
sync_group("speed", "min_speed", "max_speed")
|
|
)
|
|
```
|
|
|
|
Do not put the computed property itself in a synchronization rule. Its read slot is derived from the dependency graph; a schema or per-instance topology is rejected when synchronized dependencies do not resolve to one domain. Read-only stored dependencies need no lock slot.
|
|
|
|
`schema_property_dependency_indices<Schema, Index>()` exposes the direct dependency indices for schema consumers. `schema_dependency_graph_acyclic<Schema>()` exposes the DAG invariant; `object_schema(...)` rejects cyclic dependency graphs at compile time.
|
|
|
|
A descriptor stores Attributes and Constraints in one metadata tuple. `for_each_metadata()` traverses both kinds, `for_each_attribute()` traverses only Attributes, and `for_each_constraint()` traverses only Constraints.
|
|
|
|
## 21. Trusted accessor properties
|
|
|
|
Trusted getter:
|
|
|
|
```cpp
|
|
trusted_computed_property<&Device::value>(key<"value">)
|
|
```
|
|
|
|
Trusted getter/setter:
|
|
|
|
```cpp
|
|
trusted_accessor_property<&Device::get_value, &Device::set_value>(key<"value">)
|
|
```
|
|
|
|
These forms access the object directly rather than using synchronized dependency views. They should only be used when the caller understands the synchronization contract of those member functions.
|
|
|
|
## 22. Runtime type-erased access
|
|
|
|
`Property_Object_Base` is the dynamic adapter boundary. It is intended for GUI inspectors, serialization adapters, scripting bridges, RPC layers and other code that learns a property key only at runtime. Normal typed C++ code should prefer `read` and `write`.
|
|
|
|
```cpp
|
|
Property_Object_Base& erased = device;
|
|
```
|
|
|
|
Introspection:
|
|
|
|
```cpp
|
|
erased.runtime_object_type();
|
|
erased.runtime_property_count();
|
|
```
|
|
|
|
### 22.1 Runtime read
|
|
|
|
```cpp
|
|
auto result = erased.runtime_read("temperature", context, callback);
|
|
```
|
|
|
|
On success the callback is invoked exactly once with the schema index, key, exact `type_info` and a pointer to the current value. The value pointer is borrowed and is valid only during the callback; copy or consume it synchronously and never retain it.
|
|
|
|
For a synchronized writable property, Structive keeps the corresponding managed read lock held while the callback executes. The callback should therefore not re-enter a conflicting managed write on the same lock domain. A stored read-only property follows the intrinsic zero-lock fast path and does not acquire a mutex merely because the access is dynamic.
|
|
|
|
### 22.2 Runtime write
|
|
|
|
```cpp
|
|
auto result = erased.runtime_write("temperature", typeid(double), &value);
|
|
```
|
|
|
|
Runtime write intentionally performs no implicit conversion. `typeid(double)` must exactly match the property's declared value type, and the pointer must address a live value of that exact type for the duration of the call. The boundary copies from a `const` input. If a writable accessor cannot accept that copy input, its descriptor exposes `runtime_copy_writable == false` and runtime access returns `unsupported_runtime_write`; typed `write` still accepts move-only values when the accessor supports them. A successful runtime write uses the same managed synchronization path as typed `write`.
|
|
|
|
### 22.3 Result contract
|
|
|
|
| Result | Meaning |
|
|
| --- | --- |
|
|
| `ok` | lookup and access completed |
|
|
| `unknown_property` | no schema property has that runtime key |
|
|
| `not_readable` | the property exists but its intrinsic capability is not readable |
|
|
| `not_writable` | the property exists but its intrinsic capability is not writable |
|
|
| `unsupported_runtime_write` | the property is intrinsically writable but its accessor cannot accept the runtime copy-input boundary |
|
|
| `type_mismatch` | runtime write supplied a type different from the declared property value type |
|
|
|
|
There is no external/persistence access mode and no access-control policy in this API. An adapter decides whether it wants to expose or call runtime read/write; Structive reports only the property's intrinsic capability.
|
|
|
|
### 22.4 Why this API is deliberately low-level
|
|
|
|
The runtime boundary uses `type_info`, `void*` and a callback because the value type is unknown to the caller at compile time. Core does not impose a universal `variant`, heap-owned `any`, serialization format or conversion registry, because any of those would add ownership and conversion policy that belongs to a higher-level adapter.
|
|
|
|
This is therefore an adapter API rather than the preferred business-code API. A higher-level extension may wrap it in domain-specific value containers without changing Structive Core.
|
|
|
|
The contract is covered directly by `core/tests/runtime_api_test.cpp`, including every result code, callback metadata, exact-type writes, managed blocking behavior and the read-only zero-lock runtime fast path.
|
|
|
|
## 23. Lock policies
|
|
|
|
Default:
|
|
|
|
```cpp
|
|
Property_Object<Device, Shared_Mutex_Policy>
|
|
```
|
|
|
|
No-lock:
|
|
|
|
```cpp
|
|
Property_Object<Device, No_Lock_Policy>
|
|
```
|
|
|
|
`No_Lock_Policy` stores no real mutex array and avoids real lock objects on managed hot paths.
|
|
|
|
## 24. Raw object access
|
|
|
|
```cpp
|
|
Device& raw = device.unsafe_object();
|
|
```
|
|
|
|
or normal public member access:
|
|
|
|
```cpp
|
|
device.temperature = 30.0;
|
|
```
|
|
|
|
Both bypass the Structive managed contract.
|
|
|
|
This is intentional. Structive is cooperative structural infrastructure, not forced encapsulation.
|
|
|
|
## 25. Recommended usage rules
|
|
|
|
Use these defaults:
|
|
|
|
1. Use ordinary C++ members for storage.
|
|
2. Register only fields that belong to the structural model.
|
|
3. Prefer member-pointer typed APIs in C++ business code.
|
|
4. Use `read_only` when the Structive managed model must never write a stored property.
|
|
5. Rely on the resulting zero-lock optimization for stored read-only data.
|
|
6. Keep raw writes to read-only properties outside concurrent managed code.
|
|
7. Use synchronization groups for mutable cross-field consistency.
|
|
8. Keep validation explicit.
|
|
9. Use runtime access only for genuinely dynamic adapters.
|
|
10. Let GUI/RPC/persistence/authorization layers own their own exposure and access policy.
|