.Net中如何將一個(gè)實(shí)例的內(nèi)存二進(jìn)制內(nèi)容讀出來(超簡單方法)
在《如何計(jì)算一個(gè)實(shí)例占用多少內(nèi)存?》中我們知道一個(gè)值類型或者引用類型的實(shí)例在內(nèi)存中占多少字節(jié)。如果我們知道這段連續(xù)的字節(jié)序列的初始地址,我們就能夠?qū)⒋碓搶?shí)例的字節(jié)內(nèi)容讀取出來。在接下來的內(nèi)容中,我們將利用一個(gè)簡單的方法輸出指定實(shí)例的字節(jié)序列,并此次分析值類型和引用類型實(shí)例在內(nèi)存的布局。
一、讀取實(shí)例在內(nèi)存中的字節(jié)
如下所示的PrintBytes<T>會(huì)將指定實(shí)例在內(nèi)存中的字節(jié)輸出到控制臺(tái)上。如代碼片段所示,我們先調(diào)用《如何計(jì)算一個(gè)實(shí)例占用多少內(nèi)存?》中定義了SizeCalculator將承載實(shí)例內(nèi)容的字節(jié)數(shù)計(jì)算出來,并創(chuàng)建對(duì)應(yīng)長度的字節(jié)數(shù)組來存放讀取的字節(jié)。如果指定的變量value是一個(gè)結(jié)構(gòu)體(值類型),意味著變量會(huì)直接指向結(jié)構(gòu)體的首字節(jié)。在這種情況下,我們只需要將該變量的引用轉(zhuǎn)換成指針(void*),然后將其轉(zhuǎn)換成IntPtr對(duì)象,并作為起始地址調(diào)用Marshal的Copy方法將指定數(shù)量的字節(jié)拷貝到創(chuàng)建的字節(jié)數(shù)組就可以了。
public static class BytesPrinter { public unsafe static void PrintBytes<T>(T value) { var size = SizeCalculator.Instance.SizeOf(() => value); var bytes = new byte[size]; var pointer = Unsafe.AsPointer(ref value); IntPtr head = typeof(T).IsValueType ? new IntPtr(pointer) : *(IntPtr*)pointer - IntPtr.Size; Marshal.Copy(head, bytes, 0, size); Console.WriteLine($"[{size}]{BitConverter.ToString(bytes)}"); } public static string AsString(this IntPtr ptr) => BitConverter.ToString(BitConverter.GetBytes(ptr.ToInt64())); }
對(duì)于引用類型,整個(gè)過程就要復(fù)雜一些。此時(shí)指定的變量value指向的是目標(biāo)對(duì)象的地址,所以在將此變量引用轉(zhuǎn)換成void*指針后,還需要將其轉(zhuǎn)換成IntPtr*指針,并最終將指針的內(nèi)容(也就是目標(biāo)對(duì)象的地址)解析出來。由于變量指向的地址并非目標(biāo)實(shí)例映射內(nèi)存字節(jié)的首地址,僅僅是存儲(chǔ)方法表地址的地方,所以還需要向前移動(dòng)一個(gè)身位(IntPtr.Size)才是實(shí)例所在內(nèi)存片段的首地址。在將所需字節(jié)拷貝到創(chuàng)建的字節(jié)數(shù)組之后,我們將其格式化成字符串輸出到控制臺(tái)上。另一個(gè)AsString擴(kuò)展方法會(huì)將指定IntPtr對(duì)象表示的內(nèi)存地址輸出到控制臺(tái)上,我們會(huì)在后續(xù)的演示中使用到它。順便把《如何計(jì)算一個(gè)實(shí)例占用多少內(nèi)存?》中介紹的SizeCalculator類型定義給出來。
public class SizeCalculator { private static readonly ConcurrentDictionary<Type, int> _sizes = new(); private static readonly MethodInfo _getDefaultMethod = typeof(SizeCalculator).GetMethod(nameof(GetDefault), BindingFlags.Static | BindingFlags.NonPublic)!; public static readonly SizeCalculator Instance = new(); public int SizeOf(Type type, Func<object?>? instanceAccessor = null) { if (_sizes.TryGetValue(type, out var size)) return size; if (type.IsValueType) return _sizes.GetOrAdd(type, CalculateValueTypeInstance); object? instance; try { instance = instanceAccessor?.Invoke() ?? Activator.CreateInstance(type); } catch { throw new InvalidOperationException("The delegate to get instance must be specified."); } return _sizes.GetOrAdd(type, type => CalculateReferenceTypeInstance(type, instance)); } public int SizeOf<T>(Func<T>? instanceAccessor = null) { if (instanceAccessor is null) return SizeOf(typeof(T)); Func<object?> accessor = () => instanceAccessor(); return SizeOf(typeof(T), accessor); } public int CalculateValueTypeInstance(Type type) { var instance = GetDefaultAsObject(type); var fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(it => !it.IsStatic) .ToArray(); if (fields.Length == 0) return 0; var tupleType = typeof(ValueTuple<,>).MakeGenericType(type, type); var tupple = tupleType.GetConstructors()[0].Invoke(new object?[] { instance, instance }); var addresses = GenerateFieldAddressAccessor(tupleType.GetFields()).Invoke(tupple).OrderBy(it => it).ToArray(); return (int)(addresses[2] - addresses[0]); } public int CalculateReferenceTypeInstance(Type type, object? instance) { var fields = GetBaseTypesAndThis(type) .SelectMany(type => type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) .Where(it => !it.IsStatic).ToArray(); if (fields.Length == 0) return type.IsValueType ? 0 : 3 * IntPtr.Size; var addresses = GenerateFieldAddressAccessor(fields).Invoke(instance); var list = new List<FieldInfo>(fields); list.Insert(0, null!); fields = list.ToArray(); Array.Sort(addresses, fields); var lastFieldOffset = (int)(addresses.Last() - addresses.First()); var lastField = fields.Last(); var lastFieldSize = lastField.FieldType.IsValueType ? CalculateValueTypeInstance(lastField.FieldType) : IntPtr.Size; var size = lastFieldOffset + lastFieldSize; // Round up to IntPtr.Size int round = IntPtr.Size - 1; return ((size + round) & (~round)) + IntPtr.Size; static IEnumerable<Type> GetBaseTypesAndThis(Type? type) { while (type is not null) { yield return type; type = type.BaseType; } } } private static Func<object?, long[]> GenerateFieldAddressAccessor(FieldInfo[] fields) { var method = new DynamicMethod( name: "GetFieldAddresses", returnType: typeof(long[]), parameterTypes: new[] { typeof(object) }, m: typeof(SizeCalculator).Module, skipVisibility: true); var ilGen = method.GetILGenerator(); // var addresses = new long[fields.Length + 1]; ilGen.DeclareLocal(typeof(long[])); ilGen.Emit(OpCodes.Ldc_I4, fields.Length + 1); ilGen.Emit(OpCodes.Newarr, typeof(long)); ilGen.Emit(OpCodes.Stloc_0); // addresses[0] = address of instace; ilGen.Emit(OpCodes.Ldloc_0); ilGen.Emit(OpCodes.Ldc_I4, 0); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Conv_I8); ilGen.Emit(OpCodes.Stelem_I8); // addresses[index] = address of field[index + 1]; for (int index = 0; index < fields.Length; index++) { ilGen.Emit(OpCodes.Ldloc_0); ilGen.Emit(OpCodes.Ldc_I4, index + 1); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldflda, fields[index]); ilGen.Emit(OpCodes.Conv_I8); ilGen.Emit(OpCodes.Stelem_I8); } ilGen.Emit(OpCodes.Ldloc_0); ilGen.Emit(OpCodes.Ret); return (Func<object?, long[]>)method.CreateDelegate(typeof(Func<object, long[]>)); } private static T GetDefault<T>() where T : struct => default!; private static object? GetDefaultAsObject(Type type) => _getDefaultMethod.MakeGenericMethod(type).Invoke(null, Array.Empty<object>()); }
二、查看值類型和引用類型實(shí)例的內(nèi)存字節(jié)
在如下的代碼片段中,我們定義的結(jié)構(gòu)體FoobarStructure和類FoobarClass具有兩個(gè)字段Foo和Bar,對(duì)應(yīng)類型分別是Byte和Int32。我們分別創(chuàng)建了它們的實(shí)例,并將這兩個(gè)字段設(shè)置成255(0xFF)和65535(0xFFFF)。我們將它們作為參數(shù)調(diào)用了上面定義的PrintBytes方法。
BytesPrinter.PrintBytes(new FoobarStructure(255, 65535)); BytesPrinter.PrintBytes(new FoobarClass(255, 65535)); public struct FoobarStructure { public byte Foo; public int Bar; public FoobarStructure(byte foo, int bar) { Foo = foo; Bar = bar; } } public class FoobarClass { public byte Foo; public int Bar; public FoobarClass(byte foo, int bar) { Foo = foo; Bar = bar; } }
程序執(zhí)行后會(huì)將指定的FoobarStructure和FoobarClass實(shí)際對(duì)應(yīng)的字節(jié)輸出控制臺(tái)上。為了更好地理解該字節(jié)序列每一部分的內(nèi)容,我特意按照如下的方式添加了方括號(hào)對(duì)它們進(jìn)行了分割。從下面的內(nèi)容可以看出,雖然Byte和Int32對(duì)應(yīng)的字節(jié)數(shù)分別為1和4,但是FoobarStructure這個(gè)結(jié)構(gòu)體的字節(jié)數(shù)卻是8,三個(gè)空白字節(jié)(紅色標(biāo)記)是為了內(nèi)存對(duì)齊額外添加的“留白(Padding,紅色標(biāo)注)”。從字節(jié)的內(nèi)容還可以看出,內(nèi)存中體現(xiàn)的字段順序默認(rèn)與它們?cè)诮Y(jié)構(gòu)體中定義的順序是一致的(Foo:FF;Bar:FF-FF-00-00)。順便提一下,基元類型在內(nèi)存中是按照“小端序”存儲(chǔ)的。
[8][FF-00-00-00]-[FF-FF-00-00]
[24][00-00-00-00-00-00-00-00]-[38-39-78-B3-FD-7F-00-00]-[FF-FF-00-00-FF-00-00-00]
FoobarClass實(shí)例在內(nèi)存中的字節(jié)數(shù)要多很多,變成了24。第一組8字節(jié)是代表ObjectHeader(包含4字節(jié)用于內(nèi)存對(duì)齊的空字節(jié)),第2組8字節(jié)代表FoobarClass類型的方法表的內(nèi)存地址。兩個(gè)字段的內(nèi)容體現(xiàn)在最后一組8字節(jié)中,可以看出它們內(nèi)容與FoobarStructure不一樣,這是因?yàn)樵谀J(rèn)的情況下,結(jié)構(gòu)體采用Sequential(與定義一致),而類則采用Auto,其目的是為了滿足內(nèi)存對(duì)其規(guī)則的情況下對(duì)字段進(jìn)行重新排序,以節(jié)省內(nèi)存空間。在這里Bar字段(FF-FF-00-00)被放在Foo字段(FF)的前面。由于24是引用類型實(shí)例在內(nèi)存中的最小字節(jié)數(shù)(針對(duì)x64架構(gòu)),字段重排針對(duì)內(nèi)存的“壓縮”沒有體現(xiàn)出來。
三、存儲(chǔ)方法表地址
.NET運(yùn)行時(shí)中針對(duì)“類型”的描述信息幾乎都來自于方法表這個(gè)內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。引用類型實(shí)例在內(nèi)存中的第二部分內(nèi)容(ObjectHeader之后)存放的就是對(duì)應(yīng)方法表的地址,實(shí)例和類型就是通過這種方式關(guān)聯(lián)起來的。在C#中,我們也可以利用表示“類型句柄(Type Handle)”的RuntimeTypeHandle對(duì)象得到對(duì)應(yīng)類型方法表的地址。在如下所示的代碼片段中,我們?cè)谳敵鯢oobarClass對(duì)象的內(nèi)存字節(jié)序列后,我們進(jìn)一步獲得了FoobarClass類型的TypeHandle對(duì)象,該對(duì)象的Value屬性返回的就是方法表地址。我們調(diào)用上面定義的AsString擴(kuò)展方法將其轉(zhuǎn)換成格式化字符串后輸出到控制臺(tái)上。
[8][FF-00-00-00]-[FF-FF-00-00] [24][00-00-00-00-00-00-00-00]-[38-39-78-B3-FD-7F-00-00]-[FF-FF-00-00-FF-00-00-00]
從如下所示的輸出結(jié)果可以看出,實(shí)例內(nèi)存字節(jié)承載的和TypeHandle提供的方法表地址是一致的。
[24]00-00-00-00-00-00-00-00-38-37-78-B3-FD-7F-00-00-FF-FF-00-00-FF-00-00-00
[TypeHandle]38-37-78-B3-FD-7F-00-00
四、Object Header的內(nèi)存布局
我看到一些文檔將Object Header命名為SyncBlock Index/Number,這種命名不能算錯(cuò),但至少?zèng)]有完整地體現(xiàn)Object Header的作用以及存儲(chǔ)方式。當(dāng)我們對(duì)某個(gè)對(duì)象加鎖的時(shí)候,系統(tǒng)會(huì)使用一個(gè)名為SyncBlock的內(nèi)部數(shù)據(jù)結(jié)果與之關(guān)聯(lián),SyncBlock中會(huì)包含當(dāng)前線程ID和遞歸等級(jí)等信息。這樣的SyncBlock被保存在一個(gè)SyncBlock Table中,它在這個(gè)表中的索引會(huì)存儲(chǔ)在Object Header。
實(shí)際上SyncBlock Index只體現(xiàn)了Object Header只體現(xiàn)了Object Header的一種使用場景而已。這種將SyncBlock Index存儲(chǔ)在Object Header中實(shí)現(xiàn)的鎖被稱為 “胖鎖(Fat Lock)” ,既然有胖鎖,自然就有瘦鎖(Thin Lock),瘦鎖直接將同步信息存儲(chǔ)在Object Header中。由于不需要訪問SyncBlock Table,瘦鎖的性能要高很多。除了用于存儲(chǔ)同步信息,Object Header還可以用來緩存對(duì)象的Hash碼。上圖體現(xiàn)了Object Header典型的三種存儲(chǔ)場景:
- 瘦鎖:使用Object Header的低27位存儲(chǔ)當(dāng)前AppDomain索引(16-26)、鎖的遞歸等級(jí)(10-15)和線程ID(0-9);
- 哈希碼:使用Object Header的低26位存儲(chǔ)對(duì)象的哈希碼;
- SyncBlock Index: 使用Object Header的低26位存儲(chǔ)關(guān)聯(lián)的SyncBlock 在SyncBlock Table的索引。
為了確定Object Header存儲(chǔ)的內(nèi)容,它的高5位被預(yù)留了下來,它們分別表示:
- 27-BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX:確定存儲(chǔ)的內(nèi)容是否是哈?;蛘逽yncBlock Index;
- 28-BIT_SBLK_SPIN_LOCK:CLR使用它以原子操作的方式修改Object Header的內(nèi)容;
- 29- BIT_SBLK_GC_RESERVE :GC在執(zhí)行過程中用于標(biāo)記對(duì)象是否被固定(pined)
- 30- BIT_SBLK_FINALIZER_RUN:GC用于確定對(duì)象的析構(gòu)函數(shù)是否被調(diào)用;
- 31- BIT_SBLK_AGILE_IN_PROGRESS:在debug build下被用來確定兩個(gè)跨AppDomain應(yīng)用的對(duì)象之間是否存在死循環(huán)。
對(duì)于上圖中的第2/3中存儲(chǔ)場景下,由于BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX只能確定承載的內(nèi)容是否是哈希碼還是SyncBlock Index,我們還得使用第26位(0-base)作進(jìn)一步區(qū)分。這個(gè)比特被稱為BIT_SBLK_IS_HASHCODE,顧名思義,它表示承載得內(nèi)容是否是對(duì)象得哈希碼。
五、存儲(chǔ)“瘦鎖”
在了解了Object Header的字節(jié)布局后,我們利用我們定義的方法將對(duì)象的Object Header的內(nèi)容讀取出來,看看它的內(nèi)容是否與描述的一致。我們先來看看基于“瘦鎖”的存儲(chǔ)方式。
await Task.Yield(); PrintThreadId(); var foobar = new Foobar(); lock (foobar) { BytesPrinter.PrintBytes(foobar); lock (foobar) { BytesPrinter.PrintBytes(foobar); lock (foobar) { BytesPrinter.PrintBytes(foobar); Debugger.Break(); } } } static void PrintThreadId() { var bytes = BitConverter.GetBytes(Environment.CurrentManagedThreadId); Console.WriteLine($"Thread Id: {BitConverter.ToString(bytes)}"); } public class Foobar{}
在如下所示的演示程序中,我們定義了一個(gè)“空”的類Foobar。await Task.Yield()之后的操作將以異步的方式執(zhí)行,為了確定Object Header中是否包含當(dāng)前線程的ID,我們將線程ID以16進(jìn)制的形式輸出到控制臺(tái)上。然后我們創(chuàng)建了一個(gè)Foobar對(duì)象,然后嵌套的方式鎖定它,并在鎖定上下文中將改對(duì)象的內(nèi)存字節(jié)輸出來。
Thread Id: 06-00-00-00
[24]00-00-00-00-06-00-00-00-10-02-66-4C-FA-7F-00-00-00-00-00-00-00-00-00-00
[24]00-00-00-00-06-04-00-00-10-02-66-4C-FA-7F-00-00-00-00-00-00-00-00-00-00
[24]00-00-00-00-06-08-00-00-10-02-66-4C-FA-7F-00-00-00-00-00-00-00-00-00-00
如下所示的程序運(yùn)行后在控制臺(tái)上的輸出,我們可以看到當(dāng)前線程ID是6(采用小端字節(jié)序)。按照我們上面介紹的內(nèi)存布局,0-9這10位用來表示線程,由于三次輸出都是在同一個(gè)線程中進(jìn)行的,所以這10位比特(紅色)是一致的(0000000110),對(duì)應(yīng)的值位6,剛好是當(dāng)前線程ID。10-15這6位(紫色)表示遞歸等級(jí),解析出來值分別是0,1和2,與我們的程序正好吻合。
[0000 0][000 0000 0000] [0000 00][00 0000 0110]
[0000 0][000 0000 0000] [0000 01][00 0000 0110]
[0000 0][000 0000 0000] [0000 10][00 0000 0110]
我們?cè)谧罾飳拥膌ock語句中調(diào)用了Debugger的Break方法,所以程序會(huì)在這里停下來。如果此時(shí)我們將當(dāng)前進(jìn)程的Dump抓下來,通過執(zhí)行dumpheap -thinlock命令會(huì)將所有“瘦鎖”列出來,從輸出的嵌套等級(jí)(2)和dumpobj的顯式結(jié)果可以看出這個(gè)瘦鎖就是Foobar對(duì)象。
六、存儲(chǔ)哈希碼
我們接下來采用類似的方式演示Object Header針對(duì)哈希碼的緩存。如下面的代碼片段所示,我們創(chuàng)建了上面定義的Foobar對(duì)象,在將其內(nèi)存字節(jié)打印出來之前,我們先將其GetHashCode方法返回的哈希碼打印來。
var foobar = new Foobar(); var hashCode = foobar.GetHashCode(); PrintHashCode(hashCode); BytesPrinter.PrintBytes(foobar); static void PrintHashCode(int hashCode) { var bytes = BitConverter.GetBytes(hashCode); Console.WriteLine($"Hash Code: {BitConverter.ToString(bytes)}"); }
從下面的輸出可以看出整個(gè)Object Header的內(nèi)容應(yīng)該和哈希碼是有關(guān)系,因?yàn)橹辽倏梢钥吹角懊?個(gè)字節(jié)內(nèi)容(9D-0D-3C)的完全一致的,但是為什么最后一個(gè)字節(jié)不同呢?
Hash Code: 9D-0D-3C-03
[24]00-00-00-00-9D-0D-3C-0F-10-86-78-E0-FA-7F-00-00-00-00-00-00-00-00-00-00
再次回到上面的描述,在第二種用于存儲(chǔ)哈希碼的場景中,Object Header利用低26位來存儲(chǔ)哈希,所以我們按照如下的方式將其低26位提取出來后就會(huì)發(fā)現(xiàn)對(duì)應(yīng)的值就是哈希碼。在看前面的6位,BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX和BIT_SBLK_IS_HASHCODE位均為1,這樣就可以確定后26位存儲(chǔ)的就是哈希碼了。
0F 3C 0D 9D
00001111 00111100 00001101 10011101
00000011 00111100 00001101 10011101
03 3C 0D 9D
由于Object類型的GetHashCode方法的返回類型為Int32,如果我們重寫了這個(gè)方法,就可能導(dǎo)致ObjectHeader無法使用26位來存放哈希值。比如我們將重寫了演示實(shí)例所用的Foobar類型,讓重寫的GetHashCode返回Int32.MaxValue。
public class Foobar { public override int GetHashCode() => int.MaxValue; }
很顯然Foobar對(duì)象的哈希碼就無法存儲(chǔ)在Object Header中,如下的輸出體現(xiàn)了這一點(diǎn)。其實(shí)不管計(jì)算出來的哈希碼能否使用26個(gè)比特來表示,只要類型重寫了GetHashCode方法且沒有直接返回base.GetHashCode(),使用Object Header來緩存哈希碼的策略就會(huì)失效。這一點(diǎn)告訴我們:當(dāng)我們需要試圖去重寫某個(gè)類的GetHashCode方法,先考慮一下這個(gè)類型是否應(yīng)該定義成結(jié)構(gòu)體。
Hash Code: FF-FF-FF-7F
[24]00-00-00-00-00-00-00-00-70-27-7A-E0-FA-7F-00-00-00-00-00-00-00-00-00-00
七、存儲(chǔ)SyncBlock Index
我們使用如下的代碼來演示Object Header針對(duì)SyncBlock Index的存儲(chǔ)。在將Foobar對(duì)象創(chuàng)建出來后,我們先調(diào)用其GetHashCode方法,并在針對(duì)該對(duì)象的lock上下文中完成針對(duì)內(nèi)存字節(jié)的輸出。
var foobar = new Foobar(); foobar.GetHashCode(); lock (foobar) { BytesPrinter.PrintBytes(foobar); Debugger.Break(); } public class Foobar{}
如下所示的是程序運(yùn)行后的輸出結(jié)果,紅色標(biāo)注的正是存儲(chǔ)SyncBlock Index的Object Header的內(nèi)容。
[24]00-00-00-00-0F-00-00-08-20-BD-87-E0-FA-7F-00-00-00-00-00-00-00-00-00-00
我們按照與上面一樣的方式將這4個(gè)字節(jié)轉(zhuǎn)換成二進(jìn)制,可以確定BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX和BIT_SBLK_IS_HASHCODE位分別為1和0,所以可以確定低26位存儲(chǔ)的就是SyncBlock Index,對(duì)應(yīng)的值位15(0b111)。
08 00 00 0F
00001000 00000000 00000000 00001111
我們?cè)趌ock上下文中同樣調(diào)用了Debugger的Break方法,所以程序會(huì)在這里停下來。如果此時(shí)我們將當(dāng)前進(jìn)程的Dump抓下來,通過執(zhí)行syncblk將正在被使用的SyncBlock顯式出來,唯一的那個(gè)的Index正是15。
到此這篇關(guān)于如何將一個(gè)實(shí)例的內(nèi)存二進(jìn)制內(nèi)容讀出來的文章就介紹到這了,更多相關(guān)內(nèi)存二進(jìn)制內(nèi)容讀出來內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
.NET Core 微信小程序退款步驟——(統(tǒng)一退款)
這篇文章主要介紹了.NET Core 微信小程序退款步驟——(統(tǒng)一退款),本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09ASP.NET對(duì)IIS中的虛擬目錄進(jìn)行操作的代碼
在做系統(tǒng)開發(fā)的過程中,我們經(jīng)常會(huì)遇到用asp.net來操作IIS,如新建虛擬目錄、更改虛擬目錄的屬性、刪除虛擬目錄等操作,現(xiàn)在分析如下2012-10-10asp.net實(shí)現(xiàn)利用反射,泛型,靜態(tài)方法快速獲取表單值到Model的方法
這篇文章主要介紹了asp.net實(shí)現(xiàn)利用反射,泛型,靜態(tài)方法快速獲取表單值到Model的方法,結(jié)合實(shí)例形式分析了asp.net中反射,泛型,靜態(tài)方法給model賦值的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11asp.net 臟字典過濾問題 用正則表達(dá)式來過濾臟數(shù)據(jù)
asp.net 臟字典過濾問題 用正則表達(dá)式來過濾臟數(shù)據(jù)2009-10-10.net前臺(tái)調(diào)用后臺(tái)函數(shù)的簡單實(shí)例
這篇文章介紹了.net前臺(tái)調(diào)用后臺(tái)函數(shù)的簡單實(shí)例,有需要的朋友可以參考一下2013-09-09MVC+EasyUI+三層新聞網(wǎng)站建立 tabs標(biāo)簽制作方法(六)
這篇文章主要為大家詳細(xì)介紹了MVC+EasyUI+三層新聞網(wǎng)站建立的第六篇,教大家如何制作tabs標(biāo)簽,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07ASP.NET網(wǎng)站使用Kindeditor富文本編輯器配置步驟
首先下載編輯器然后部署編輯器最后在網(wǎng)頁中加入(ValidateRequest="false")引入腳本文件,具體配置步驟如下,有需求的朋友可以了解下哈2013-06-06為HttpClient添加默認(rèn)請(qǐng)求報(bào)頭的四種解決方案
這篇文章主要給大家介紹了關(guān)于為HttpClient添加默認(rèn)請(qǐng)求報(bào)頭的四種解決方案,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用HttpClient具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09