月份: 2020 年 2 月

  • 坑~夏令時冬令時引發的時間換算問題

     

    起因

    最近接觸到一些國外的項目,由於國內外有時差這個東西,對於某些基礎數據存到數據庫的時候需要記錄時間,為了方便,這裏採用了時間戳(int或者timestamp)記錄。由於時間戳全球都是一樣的,需要的時候根據時區進行轉換就能夠拿到當地的時間。

    嗯~ o(* ̄▽ ̄*)o,這樣看起來確實沒什麼毛病。眾所周知,一天有24小時,換算成秒就是:24*60*60=86400秒。

    然而,我在某次使用 MySql 的 FROM_UNIXTIME 發現一個問題,兩個時間相差86400秒,但是格式化之後卻不是相差一天!!!

    假設北京時間2019年11月25日 12:00:00,對應的時間戳是:1574654400,照理說這個時間戳加上一天86400秒,理論上就是北京時間2019年11月26日 12:00:00,事實上確實如此,國內的話這麼算確實沒什麼問題,但是如果是國外時區的話,那可能會出問題。

    由於國外部分國家有夏令時冬令時之分(具體下面會細說),直接加上86400秒可能會有問題。

    感興趣的可以拿1572764400(太平洋時間2019-11-03 00:00:00,單位:)這個時間戳驗證下

    拿代碼演示下:

    PHP:

    <?php
    
    echo "PST時區的時間\n";
    date_default_timezone_set('PST8PDT');
    echo date('Y-m-d H:i:s',1572764400);
    echo "\n";
    echo date('Y-m-d H:i:s',1572764400+86400);
    echo "\n";
    
    //換個時區
    echo "換成上海時區看看\n";
    date_default_timezone_set('Asia/Shanghai');
    echo date('Y-m-d H:i:s',1572764400);
    echo "\n";
    echo date('Y-m-d H:i:s',1572764400+86400);
    echo "\n";

    運行結果:

    PST時區的時間
    2019-11-03 00:00:00
    2019-11-03 23:00:00
    換成上海時區看看
    2019-11-03 15:00:00
    2019-11-04 15:00:00

    明明是同一個時間戳,都是加上86400(一天),為什麼在上海這個時區是第二天,而在PST(美國太平洋時區)只加了23小時?神不神奇!意不意外!

    為了弄清楚這個問題,首先得先了解下什麼是夏令時,什麼是冬令時

     

    夏令時

    夏令時,表示為了節約能源,人為規定時間的意思。也叫夏時制,夏時令(Daylight Saving Time:DST),又稱“日光節約時制”和“夏令時間”,在這一制度實行期間所採用的統一時間稱為“夏令時間”。

    一般在天亮早的夏季人為將時間調快一小時,可以使人早起早睡,減少照明量,以充分利用光照資源,從而節約照明用電。各個採納夏時制的國家具體規定不同。目前全世界有近110個國家每年要實行夏令時。[1]

     

    冬令時

    有夏令時就會有冬令時。高緯度和中緯度的許多國家在夏季到來前,把時針撥快一小時,新的時間就是夏令時,到下半季秋季來臨前,再把時針撥回一小時,即形成冬令時。 [2] 

     

    夏令時和冬令時的影響

    拿美國來說,美國各個地區的時間都不同,不像中國一樣統一使用北京時間,美國一般以三月份第二個周日凌晨兩點當成夏季的開始,十一月份第一個周日的凌晨兩點當成冬季的開始。

    所以在每年的三月份第二個周日凌晨兩點過後,時間就會往前調快一個小時;同理,十一月份第一個周日把這一個小時調回來

    你也可以理解成美國那邊,一年裡面有一天只有23小時(夏天開始那一天),有一天有25小時(冬天開始那一天),其他時間每天都是24小時。

    所以你會發現,夏天的時候,中國的北京時間(東八區)與美國太平洋時區(西八區)的時差是15小時,而到了冬天卻變成16小時

     

    解決方案

    回到開頭那個問題,如果我們想直接算第二天,直接加上86400(一天)可能在其他國家就會有我上面那個夏令時和冬令時時間換算的問題,要如何避免呢?首先能夠確定的是,直接加上86400是不可取的,如果加上一天能否行得通

    PHP:

    <?php
    
    echo "PST時區的時間\n";
    date_default_timezone_set('PST8PDT');
    echo date('Y-m-d H:i:s',1572764400);
    echo "\n";
    echo date('Y-m-d H:i:s',1572764400+86400);
    echo "\n";
    
    echo "--------------------------\n";
    echo date('Y-m-d H:i:s',1572764400);
    echo "\n";
    echo date('Y-m-d H:i:s',strtotime('+1 day',1572764400));
    echo "\n";

    運行結果:

    PST時區的時間
    2019-11-03 00:00:00
    2019-11-03 23:00:00
    --------------------------
    2019-11-03 00:00:00
    2019-11-04 00:00:00

    可以看出,不直接加上86400,直接在日期上加上一天是完全沒問題的。

    JavaScript:

    var date = new Date(1572764400*1000);
    date.setDate(date.getDate()+1);
    var timestamp = Math.round(date.getTime()/1000);

    注意:JS的時間戳是毫秒!!!

     

    結論

    在經濟全球化快速發展的今天,在軟件開發的過程中,盡量養成習慣,由於夏令時和冬令時不是固定的,開發在時間計算上應該慎用86400進行加減運算,時間計算請直接對日期進行加減,展示時間給用戶看的時候盡量結合當地時間,結合夏令時和冬令時計算出準確的當地時間,避免產生不必要的分歧。

     

    參考:

    [1]. 

    [2]. 

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • .NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

    .NET高級特性-Emit(2)類的定義,.NET高級特性-Emit(1)

      在上一篇博文發了一天左右的時間,就收到了博客園許多讀者的評論和推薦,非常感謝,我也會及時回復讀者的評論。之後我也將繼續撰寫博文,梳理相關.NET的知識,希望.NET的圈子能越來越大,開發者能了解/深入.NET的本質,將工作做的簡單又高效,拒絕重複勞動,拒絕CRUD。

      ok,咱們開始繼續Emit的探索。在這之前,我先放一下我往期關於Emit的文章,方便讀者閱讀。

      《》

    一、基礎知識

      既然C#作為一門面向對象的語言,所以首當其沖的我們需要讓Emit為我們動態構建類。

      廢話不多說,首先,我們先來回顧一下C#類的內部由什麼東西組成:

      (1) 字段-C#類中保存數據的地方,由訪問修飾符、類型和名稱組成;

      (2) 屬性-C#類中特有的東西,由訪問修飾符、類型、名稱和get/set訪問器組成,屬性的是用來控制類中字段數據的訪問,以實現類的封裝性;在Java當中寫作getXXX()和setXXX(val),C#當中將其變成了屬性這種語法糖;

      (3) 方法-C#類中對邏輯進行操作的基本單元,由訪問修飾符、方法名、泛型參數、入參、出參構成;

      (4) 構造器-C#類中一種特殊的方法,該方法是專門用來創建對象的方法,由訪問修飾符、與類名相同的方法名、入參構成。

      接着,我們再觀察C#類本身又具備哪些東西:

      (1) 訪問修飾符-實現對C#類的訪問控制

      (2) 繼承-C#類可以繼承一個父類,並需要實現父類當中所有抽象的方法以及選擇實現父類的虛方法,還有就是子類需要調用父類的構造器以實現對象的創建

      (3) 實現-C#類可以實現多個接口,並實現接口中的所有方法

      (4) 泛型-C#類可以包含泛型參數,此外,類還可以對泛型實現約束

      以上就是C#類所具備的一些元素,以下為樣例:

    public abstract class Bar
    {
        public abstract void PrintName();
    }
    public interface IFoo<T> { public T Name { get; set; } } //繼承Bar基類,實現IFoo接口,泛型參數T
    public class Foo<T> : Bar, IFoo<T>
      //泛型約束
      where T : struct {
    //構造器 public Foo(T name):base() { _name = name; } //字段 private T _name; //屬性 public T Name { get => _name; set => _name = value; } //方法 public override void PrintName() {
        Console.WriteLine(_name.ToString()); }
    }

      在探索完了C#類及其定義后,我們要來了解C#的項目結構組成。我們知道C#的一個csproj項目最終會對應生成一個dll文件或者exe文件,這一個文件我們稱之為程序集Assembly;而在一個程序集中,我們內部包含和定義了許多命名空間,這些命令空間在C#當中被稱為模塊Module,而模塊正是由一個一個的C#類Type組成。

     

     

     

       所以,當我們需要定義C#類時,就必須首先定義Assembly以及Module,如此才能進行下一步工作。

    二、IL概覽

       由於Emit實質是通過IL來生成C#代碼,故我們可以反向生成,先將寫好的目標代碼寫成cs文件,通過編譯器生成dll,再通過ildasm查看IL代碼,即可依葫蘆畫瓢的編寫出Emit代碼。所以我們來查看以下上節Foo所生成的IL代碼。

      

     

     

       從上圖我們可以很清晰的看到.NET的層級結構,位於樹頂層淺藍色圓點表示一個程序集Assembly,第二層藍色表示模塊Module,在模塊下的均為我們所定義的類,類中包含類的泛型參數、繼承類信息、實現接口信息,類的內部包含構造器、方法、字段、屬性以及它的get/set方法,由此,我們可以開始編寫Emit代碼了

    三、Emit編寫

      有了以上的對C#類的解讀和IL的解讀,我們知道了C#類本身所需要哪些元素,我們就開始根據這些元素來開始編寫Emit代碼了。這裏的代碼量會比較大,請讀者慢慢閱讀,也可以參照以上我寫的類生成il代碼進行比對。

      在Emit當中所有創建類型的幫助類均以Builder結尾,從下錶中我們可以看的非常清楚

    元素中文 元素名稱 對應Emit構建器名稱
    程序集  Assembly AssemblyBuilder
    模塊  Module ModuleBuilder
     Type TypeBuilder
    構造器  Constructor ConstructorBuilder
    屬性  Property PropertyBuilder
    字段  Field FieldBuilder
    方法  Method MethodBuilder

      由於創建類需要從Assembly開始創建,所以我們的入口是AssemblyBuilder

      (1) 首先,我們先引入命名空間,我們以上節Foo類為樣例進行編寫

    using System.Reflection.Emit;

      (2) 獲取基類和接口的類型

    var barType = typeof(Bar);
    var interfaceType = typeof(IFoo<>);

      (3) 定義Foo類型,我們可以看到在定義類之前我們需要創建Assembly和Module

    //定義類
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Edwin.Blog.Emit"), AssemblyBuilderAccess.Run);
    var moduleBuilder = assemblyBuilder.DefineDynamicModule("Edwin.Blog.Emit");
    var typeBuilder = moduleBuilder.DefineType("Foo", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit);

      (4) 定義泛型參數T,並添加約束

    //定義泛型參數
    var genericTypeBuilder = typeBuilder.DefineGenericParameters("T")[0];
    //設置泛型約束
    genericTypeBuilder.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

      (5) 繼承和實現接口,注意當實現類的泛型參數需傳遞給接口時,需要將泛型接口添加泛型參數后再調用AddInterfaceImplementation方法

    //繼承基類
    typeBuilder.SetParent(barType);
    //實現接口
    typeBuilder.AddInterfaceImplementation(interfaceType.MakeGenericType(genericTypeBuilder));

      (6) 定義字段,因為字段在構造器值需要使用,故先創建

    //定義字段
    var fieldBuilder = typeBuilder.DefineField("_name", genericTypeBuilder, FieldAttributes.Private);

      (7) 定義構造器,並編寫內部邏輯

    //定義構造器
    var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, CallingConventions.Standard, new Type[] { genericTypeBuilder });
    var ctorIL = ctorBuilder.GetILGenerator();
    //Ldarg_0在實例方法中表示this,在靜態方法中表示第一個參數
    ctorIL.Emit(OpCodes.Ldarg_0);
    ctorIL.Emit(OpCodes.Ldarg_1);
    //為field賦值
    ctorIL.Emit(OpCodes.Stfld, fieldBuilder);
    ctorIL.Emit(OpCodes.Ret);

      (8) 定義Name屬性

    //定義屬性
    var propertyBuilder = typeBuilder.DefineProperty("Name", PropertyAttributes.None, genericTypeBuilder, Type.EmptyTypes);

      (9) 編寫Name屬性的get/set訪問器

    //定義get方法
    var getMethodBuilder = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, genericTypeBuilder, Type.EmptyTypes);
    var getIL = getMethodBuilder.GetILGenerator();
    getIL.Emit(OpCodes.Ldarg_0);
    getIL.Emit(OpCodes.Ldfld, fieldBuilder);
    getIL.Emit(OpCodes.Ret);
    typeBuilder.DefineMethodOverride(getMethodBuilder, interfaceType.GetProperty("Name").GetGetMethod()); //實現對接口方法的重載
    propertyBuilder.SetGetMethod(getMethodBuilder); //設置為屬性的get方法
    //定義set方法
    var setMethodBuilder = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName | MethodAttributes.Virtual, CallingConventions.Standard, null, new Type[] { genericTypeBuilder });
    var setIL = setMethodBuilder.GetILGenerator();
    setIL.Emit(OpCodes.Ldarg_0);
    setIL.Emit(OpCodes.Ldarg_1);
    setIL.Emit(OpCodes.Stfld, fieldBuilder);
    setIL.Emit(OpCodes.Ret);
    typeBuilder.DefineMethodOverride(setMethodBuilder, interfaceType.GetProperty("Name").GetSetMethod()); //實現對接口方法的重載
    propertyBuilder.SetSetMethod(setMethodBuilder); //設置為屬性的set方法

       (10) 定義並實現PrintName方法

    //定義方法
    var printMethodBuilder = typeBuilder.DefineMethod("PrintName", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.Standard, null, Type.EmptyTypes);
    var printIL = printMethodBuilder.GetILGenerator();
    printIL.Emit(OpCodes.Ldarg_0);
    printIL.Emit(OpCodes.Ldflda, fieldBuilder);
    printIL.Emit(OpCodes.Constrained, genericTypeBuilder);
    printIL.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
    printIL.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
    printIL.Emit(OpCodes.Ret);
    //實現對基類方法的重載
    typeBuilder.DefineMethodOverride(printMethodBuilder, barType.GetMethod("PrintName", Type.EmptyTypes));

      (11) 創建類

    var type = typeBuilder.CreateType(); //netstandard中請使用CreateTypeInfo().AsType()

      (12) 調用

    var obj = Activator.CreateInstance(type.MakeGenericType(typeof(DateTime)), DateTime.Now);
    (obj as Bar).PrintName();
    Console.WriteLine((obj as IFoo<DateTime>).Name);

    四、應用

      上面的樣例僅供學習只用,無法運用在實際項目當中,那麼,Emit構建類在實際項目中我們可以有什麼應用,提高我們的編碼效率

      (1) 動態DTO-當我們需要將實體映射到某個DTO時,可以用動態DTO來代替你手寫的DTO,選擇你需要的字段回傳給前端,或者前端把他想要的字段傳給後端

      (2) DynamicLinq-我的第一篇博文有個讀者提到了表達式樹,而linq使用的正是表達式樹,當表達式樹+Emit時,我們就可以用像SQL或者GraphQL那樣的查詢語句實現動態查詢

      (3) 對象合併-我們可以編寫實現一個像js當中Object.assign()一樣的方法,實現對兩個實體的合併

      (4) AOP動態代理-AOP的核心就是代理模式,但是與其對應的是需要手寫代理類,而Emit就可以幫你動態創建代理類,實現切面編程

      (5) …

    五、小結

      對於Emit,確實初學者會對其感到複雜和難以學習,但是只要搞懂其中的原理,其實最終就是C#和.NET語言的本質所在,在學習Emit的同時,也是在鍛煉你的基本功是否紮實,你是否對這門語言精通,是否有各種簡化代碼的應用。

      保持學習,勇於實踐;Write Less,Do More;作者之後還會繼續.NET高級特性系列,感謝閱讀!

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

    ※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

  • 標準庫bufio個人詳解

    標準庫bufio個人詳解

    本文是我有通俗的語言寫的如果有誤請指出。

    先看bufio官方文檔

    https://studygolang.com/pkgdoc文檔地址

     

     主要分三部分Reader、Writer、Scanner

    分別是讀數據、寫數據和掃描器三種數據類型的相關操作 這個掃描後面會詳細說我開始也沒弄明白其實很簡單。

     

    Reader

    func 

    func NewReaderSize(rd ., size ) *

    NewReaderSize創建一個具有最少有size尺寸的緩衝、從r讀取的*Reader。如果參數r已經是一個具有足夠大緩衝的* Reader類型值,會返回r。

     

     

     解釋:看官方解釋這個方法可能不太容易懂,這個意思就是就是你可以給*Reader自定義一個size大小的緩衝區,*Reader每次從底層io.Reader(也就是你那個文件或者流)中預讀size大小的數據到緩衝區中(可能讀不滿),然後你每次讀數據實際是從這個緩衝區中拿數據。

     

     下面是NewReaderSize源碼

    func NewReaderSize(rd io.Reader, size int) *Reader {
        // Is it already a Reader?
        b, ok := rd.(*Reader)
        if ok && len(b.buf) >= size {
            return b
        }
        if size < minReadBufferSize { //minReadBufferSize==16
            size = minReadBufferSize
        }
        r := new(Reader)
        r.reset(make([]byte, size), rd)
        return r
    }
    

      r.reset 初始化了一個*Reader 返回大小是size。

    func 

    func NewReader(rd .) *

    NewReader創建一個具有默認大小緩衝、從r讀取的*Reader。

    解釋:那這個NewReader就很好解釋了 和NewReaderSize基本一樣就是緩衝區大小是默認設置好的

    func (*Reader) 

    func (b *) Peek(n ) ([], )

    解釋:Peek就是返回緩存的一個切片,該切片引用緩存中的前N個字節的數據,如果n大於總大小,則返回能讀到的字節數的數據。

    func (*Reader) 

    func (b *) Read(p []) (n , err )

    Read讀取數據寫入p。本方法返回寫入p的字節數。本方法一次調用最多會調用下層Reader接口一次Read方法,因此返回值n可能小於len(p)。讀取到達結尾時,返回值n將為0而err將為io.EOF。

    解釋:如果緩存不為空則直接從緩存中讀數據不會從底層io.Reader讀,如果緩存為空len(p)>緩存大小,則直接從底層io.Reader讀數據到p。

    如果len(p)<緩存大小,則先從底層io.Reader中讀數據到緩存再到p。

     

    主要就這幾個 還有幾個文檔寫的都很清楚易懂我就不多寫了。

    Writer類型的方法和Reader類型的方法差不多也很易懂主要就一個Flush要注意。

    func (*Writer) 

    func (b *) Flush() 

    Flush方法將緩衝中的數據寫入下層的io.Writer接口。

    和Reader是倒過來的,Writer每次寫數據是先寫入緩衝區的,進程緩衝區填滿后,通過進程緩衝寫入到內核緩衝再寫入到磁盤,使用Flush就不等填滿直接走寫入流程了,保證你的數據及時寫入文件。

     

     

     

     解釋:scanner類型掃描器 官方的說法很複雜,我也沒太看懂找了很多資料,其實就是你在數據傳輸的時候時候使用“分隔符”,scanner類型可以通過分隔符逐個迭代你的數據。

    上面4個函數func Scan……  就是分隔符的判斷函數這4個是給你預設好的,你也可以按照自己的需求改寫。

    怎麼改寫呢,看下面

    func (*Scanner) 

    func (s *) Split(split )

    這個Split方法就是設置你這個scanner的用哪個SplitFunc類型的函數

    在看下面這個SpliFunc類型的函數簽名

    type SplitFunc func(data [], atEOF ) (advance , token [], err )

    照着這個格式寫一個不就得了么,當然具體寫法給出了但是你不會?沒關係咱看一下官方是咋寫的。

    https://github.com/golang/go/blob/master/src/bufio/scan.go?name=release#57官方源碼地址

    func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
    	if atEOF && len(data) == 0 {
    		return 0, nil, nil
    	}
    	if i := bytes.IndexByte(data, '\n'); i >= 0 {
    		// We have a full newline-terminated line.
    		return i + 1, dropCR(data[0:i]), nil
    	}
    	// If we're at EOF, we have a final, non-terminated line. Return it.
    	if atEOF {
    		return len(data), dropCR(data), nil
    	}
    	// Request more data.
    	return 0, nil, nil
    }
    

       

    看bytes.IndexByte(data, ‘\n’);這段不就是在找行尾嘛 比如你想改成以“;”為分隔符的就改成bytes.IndexByte(data, ‘;’);不就得了么

    func main(){
        scanner:=bufio.NewScanner(
            strings.NewReader("abcdefg\nhigklmn"),
        )
        scanner.Split(ScanLines) //這裏可以隨意選擇用哪個函數也可以自定義,可以不指定默認為\n做分隔符
      for scanner.Scan(){
        fmt.Println(scanner.Text())
      }
    }

      

    到此為止拉~

     

     

     

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!