1 module dls.util.nullable; 2 3 import std.format : singleSpec, FormatSpec, formatValue; 4 import std.meta : AliasSeq, allSatisfy; 5 import std.range.primitives : isOutputRange; 6 import std.traits; 7 8 struct Nullable(T) 9 { 10 private union DontCallDestructorT 11 { 12 T payload; 13 } 14 15 private DontCallDestructorT _value = DontCallDestructorT.init; 16 17 private bool _isNull = true; 18 19 /** 20 * Constructor initializing `this` with `value`. 21 * 22 * Params: 23 * value = The value to initialize this `Nullable` with. 24 */ 25 this(inout T value) inout 26 { 27 _value.payload = value; 28 _isNull = false; 29 } 30 31 static if (is(T == struct) && hasElaborateDestructor!T) 32 { 33 ~this() 34 { 35 if (!_isNull) 36 { 37 destroy(_value.payload); 38 } 39 } 40 } 41 42 /** 43 * If they are both null, then they are equal. If one is null and the other 44 * is not, then they are not equal. If they are both non-null, then they are 45 * equal if their values are equal. 46 */ 47 bool opEquals()(auto ref const(typeof(this)) rhs) const 48 { 49 if (_isNull) 50 return rhs._isNull; 51 if (rhs._isNull) 52 return false; 53 return _value.payload == rhs._value.payload; 54 } 55 56 /// Ditto 57 bool opEquals(U)(auto ref const(U) rhs) const 58 if (!is(U : typeof(this)) && is(typeof(this.get == rhs))) 59 { 60 return _isNull ? false : rhs == _value.payload; 61 } 62 63 /// 64 @safe unittest 65 { 66 Nullable!int empty; 67 Nullable!int a = 42; 68 Nullable!int b = 42; 69 Nullable!int c = 27; 70 71 assert(empty == empty); 72 assert(empty == Nullable!int.init); 73 assert(empty != a); 74 assert(empty != b); 75 assert(empty != c); 76 77 assert(a == b); 78 assert(a != c); 79 80 assert(empty != 42); 81 assert(a == 42); 82 assert(c != 42); 83 } 84 85 @safe unittest 86 { 87 // Test constness 88 immutable Nullable!int a = 42; 89 Nullable!int b = 42; 90 immutable Nullable!int c = 29; 91 Nullable!int d = 29; 92 immutable e = 42; 93 int f = 29; 94 assert(a == a); 95 assert(a == b); 96 assert(a != c); 97 assert(a != d); 98 assert(a == e); 99 assert(a != f); 100 101 // Test rvalue 102 assert(a == const Nullable!int(42)); 103 assert(a != Nullable!int(29)); 104 } 105 106 // https://issues.dlang.org/show_bug.cgi?id=17482 107 @system unittest 108 { 109 import std.variant : Variant; 110 Nullable!Variant a = Variant(12); 111 assert(a == 12); 112 Nullable!Variant e; 113 assert(e != 12); 114 } 115 116 size_t toHash() const @safe nothrow 117 { 118 static if (__traits(compiles, .hashOf(_value.payload))) 119 return _isNull ? 0 : .hashOf(_value.payload); 120 else 121 // Workaround for when .hashOf is not both @safe and nothrow. 122 return _isNull ? 0 : typeid(T).getHash(&_value.payload); 123 } 124 125 /** 126 * Gives the string `"Nullable.null"` if `isNull` is `true`. Otherwise, the 127 * result is equivalent to calling $(REF formattedWrite, std,format) on the 128 * underlying value. 129 * 130 * Params: 131 * writer = A `char` accepting 132 * $(REF_ALTTEXT output range, isOutputRange, std, range, primitives) 133 * fmt = A $(REF FormatSpec, std,format) which is used to represent 134 * the value if this Nullable is not null 135 * Returns: 136 * A `string` if `writer` and `fmt` are not set; `void` otherwise. 137 */ 138 string toString() 139 { 140 import std.array : appender; 141 auto app = appender!string(); 142 auto spec = singleSpec("%s"); 143 toString(app, spec); 144 return app.data; 145 } 146 147 /// ditto 148 string toString() const 149 { 150 import std.array : appender; 151 auto app = appender!string(); 152 auto spec = singleSpec("%s"); 153 toString(app, spec); 154 return app.data; 155 } 156 157 /// ditto 158 void toString(W)(ref W writer, scope const ref FormatSpec!char fmt) 159 if (isOutputRange!(W, char)) 160 { 161 import std.range.primitives : put; 162 if (isNull) 163 put(writer, "Nullable.null"); 164 else 165 formatValue(writer, _value.payload, fmt); 166 } 167 168 /// ditto 169 void toString(W)(ref W writer, scope const ref FormatSpec!char fmt) const 170 if (isOutputRange!(W, char)) 171 { 172 import std.range.primitives : put; 173 if (isNull) 174 put(writer, "Nullable.null"); 175 else 176 formatValue(writer, _value.payload, fmt); 177 } 178 179 /** 180 * Check if `this` is in the null state. 181 * 182 * Returns: 183 * true $(B iff) `this` is in the null state, otherwise false. 184 */ 185 @property bool isNull() const @safe pure nothrow 186 { 187 return _isNull; 188 } 189 190 /// 191 @safe unittest 192 { 193 Nullable!int ni; 194 assert(ni.isNull); 195 196 ni = 0; 197 assert(!ni.isNull); 198 } 199 200 // https://issues.dlang.org/show_bug.cgi?id=14940 201 @safe unittest 202 { 203 import std.array : appender; 204 import std.format : formattedWrite; 205 206 auto app = appender!string(); 207 Nullable!int a = 1; 208 formattedWrite(app, "%s", a); 209 assert(app.data == "1"); 210 } 211 212 // https://issues.dlang.org/show_bug.cgi?id=19799 213 @safe unittest 214 { 215 import std.format : format; 216 217 const Nullable!string a = const(Nullable!string)(); 218 219 format!"%s"(a); 220 } 221 222 /** 223 * Forces `this` to the null state. 224 */ 225 void nullify()() 226 { 227 static if (is(T == class) || is(T == interface)) 228 _value.payload = null; 229 else 230 .destroy(_value.payload); 231 _isNull = true; 232 } 233 234 /// 235 @safe unittest 236 { 237 Nullable!int ni = 0; 238 assert(!ni.isNull); 239 240 ni.nullify(); 241 assert(ni.isNull); 242 } 243 244 /** 245 * Assigns `value` to the internally-held state. If the assignment 246 * succeeds, `this` becomes non-null. 247 * 248 * Params: 249 * value = A value of type `T` to assign to this `Nullable`. 250 */ 251 void opAssign()(T value) 252 { 253 import std.algorithm.mutation : moveEmplace, move; 254 255 // the lifetime of the value in copy shall be managed by 256 // this Nullable, so we must avoid calling its destructor. 257 auto copy = DontCallDestructorT(value); 258 259 if (_isNull) 260 { 261 // trusted since payload is known to be T.init here. 262 () @trusted { moveEmplace(copy.payload, _value.payload); }(); 263 } 264 else 265 { 266 move(copy.payload, _value.payload); 267 } 268 _isNull = false; 269 } 270 271 /** 272 * If value is null, sets this to null, otherwise assigns 273 * `value.get` to the internally-held state. If the assignment 274 * succeeds, `this` becomes non-null. 275 * 276 * Params: 277 * value = A value of type `Nullable!T` to assign to this `Nullable`. 278 */ 279 void opAssign()(Nullable!T value) 280 { 281 if (value._isNull) 282 nullify(); 283 else 284 opAssign(value.get()); 285 } 286 287 /** 288 * If this `Nullable` wraps a type that already has a null value 289 * (such as a pointer), then assigning the null value to this 290 * `Nullable` is no different than assigning any other value of 291 * type `T`, and the resulting code will look very strange. It 292 * is strongly recommended that this be avoided by instead using 293 * the version of `Nullable` that takes an additional `nullValue` 294 * template argument. 295 */ 296 @safe unittest 297 { 298 //Passes 299 Nullable!(int*) npi; 300 assert(npi.isNull); 301 302 //Passes?! 303 npi = null; 304 assert(!npi.isNull); 305 } 306 307 /** 308 * Gets the value if not null. If `this` is in the null state, and the optional 309 * parameter `fallback` was provided, it will be returned. Without `fallback`, 310 * calling `get` with a null state is invalid. 311 * 312 * When the fallback type is different from the Nullable type, `get(T)` returns 313 * the common type. 314 * 315 * Params: 316 * fallback = the value to return in case the `Nullable` is null. 317 * 318 * Returns: 319 * The value held internally by this `Nullable`. 320 */ 321 @property ref inout(T) get() inout @safe pure nothrow 322 { 323 enum message = "Called `get' on null Nullable!" ~ T.stringof ~ "."; 324 assert(!isNull, message); 325 return _value.payload; 326 } 327 328 /// ditto 329 @property inout(T) get()(inout(T) fallback) inout @safe pure nothrow 330 { 331 return isNull ? fallback : _value.payload; 332 } 333 334 /// ditto 335 @property auto get(U)(inout(U) fallback) inout @safe pure nothrow 336 { 337 return isNull ? fallback : _value.payload; 338 } 339 340 //@@@DEPRECATED_2.096@@@ 341 deprecated( 342 "Implicit conversion with `alias Nullable.get this` will be removed after 2.096. Please use `.get` explicitly.") 343 @system unittest 344 { 345 import core.exception : AssertError; 346 import std.exception : assertThrown, assertNotThrown; 347 348 Nullable!int ni; 349 int i = 42; 350 //`get` is implicitly called. Will throw 351 //an AssertError in non-release mode 352 assertThrown!AssertError(i = ni); 353 assert(i == 42); 354 355 ni = 5; 356 assertNotThrown!AssertError(i = ni); 357 assert(i == 5); 358 } 359 360 //@@@DEPRECATED_2.096@@@ 361 deprecated( 362 "Implicit conversion with `alias Nullable.get this` will be removed after 2.096. Please use `.get` explicitly.") 363 @property ref inout(T) get_() inout @safe pure nothrow 364 { 365 return get; 366 } 367 368 /// 369 @safe pure nothrow unittest 370 { 371 int i = 42; 372 Nullable!int ni; 373 int x = ni.get(i); 374 assert(x == i); 375 376 ni = 7; 377 x = ni.get(i); 378 assert(x == 7); 379 } 380 381 /** 382 * Implicitly converts to `T`. 383 * `this` must not be in the null state. 384 * This feature is deprecated and will be removed after 2.096. 385 */ 386 alias get_ this; 387 } 388 389 /// ditto 390 auto nullable(T)(T t) 391 { 392 return Nullable!T(t); 393 }